{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal",
  "type": "registry:component",
  "title": "Terminal",
  "description": "An animated terminal window that types each command and prints its output line by line, with a blinking cursor and a replay button.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/terminal.tsx",
      "type": "registry:ui",
      "target": "components/ui/terminal.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { useInView, useReducedMotion } from \"motion/react\"\nimport { RotateCcwIcon } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * An animated terminal window that types each command and prints its output line by line, with a blinking cursor and a replay button.\n * Five line kinds (command, output, success, error, info) each get their own\n * token colour and glyph. Typing pauses while the window is off screen and\n * picks up where it left off; reduced motion prints the whole transcript at\n * once. The box reserves its full height, so nothing below it shifts.\n */\ntype TerminalLine = {\n  /** Commands are typed character by character. Every other kind prints whole. */\n  kind?: \"command\" | \"output\" | \"success\" | \"error\" | \"info\"\n  text: string\n  /** Milliseconds to wait before this line starts, overriding the component defaults. */\n  delay?: number\n}\n\ntype TerminalProps = {\n  lines?: TerminalLine[]\n  title?: string\n  /** Prompt printed before each command. */\n  prompt?: string\n  /** Milliseconds per typed character. */\n  typingSpeed?: number\n  /** Milliseconds before each output line prints. */\n  lineDelay?: number\n  /** Milliseconds before a command starts typing. */\n  commandDelay?: number\n  /** Start over after the last line, with a pause. */\n  loop?: boolean\n  /** Show the replay button in the title bar. */\n  replay?: boolean\n  /** Stay dark in both themes, or follow the page theme. */\n  appearance?: \"dark\" | \"auto\"\n  /** Called once the last line has printed. */\n  onComplete?: () => void\n  className?: string\n}\n\nconst SAMPLE_LINES: TerminalLine[] = [\n  { kind: \"command\", text: \"npx shadcn@latest add https://ui.minidev.pro/r/terminal.json\" },\n  { kind: \"success\", text: \"Checking registry.\" },\n  { kind: \"success\", text: \"Installing dependencies.\" },\n  { kind: \"success\", text: \"Created 1 file:\" },\n  { kind: \"output\", text: \"    components/ui/terminal.tsx\" },\n  { kind: \"command\", text: \"npm run dev\", delay: 900 },\n  { kind: \"info\", text: \"Next.js 16.0.0 (Turbopack)\" },\n  { kind: \"output\", text: \"  Local:    http://localhost:3000\" },\n  { kind: \"success\", text: \"Ready in 412ms\" },\n]\n\ntype Pos = { line: number; char: number }\n\nconst GLYPH: Record<string, { mark: string; className: string }> = {\n  success: { mark: \"✓\", className: \"text-success\" },\n  error: { mark: \"✕\", className: \"text-danger\" },\n  info: { mark: \"›\", className: \"text-info\" },\n  output: { mark: \" \", className: \"\" },\n}\n\nconst LINE_TEXT: Record<string, string> = {\n  success: \"text-fg\",\n  error: \"text-danger\",\n  info: \"text-fg\",\n  output: \"text-fg-muted\",\n}\n\n/** Deterministic rhythm for keystrokes: no Math.random, so every replay feels the same. */\nfunction jitter(i: number) {\n  const s = Math.sin(i * 12.9898) * 43758.5453\n  return 0.7 + 0.6 * (s - Math.floor(s))\n}\n\n/** Light syntax tint for a command: binary, flags and URLs. */\nfunction tokenize(text: string) {\n  const parts: { t: string; c: string }[] = []\n  let first = true\n  for (const m of text.match(/\\s+|\\S+/g) ?? []) {\n    if (/^\\s+$/.test(m)) parts.push({ t: m, c: \"\" })\n    else if (first) {\n      parts.push({ t: m, c: \"font-medium text-fg\" })\n      first = false\n    } else if (m.includes(\"://\")) parts.push({ t: m, c: \"text-info\" })\n    else if (m.startsWith(\"-\")) parts.push({ t: m, c: \"text-fg-muted\" })\n    else parts.push({ t: m, c: \"text-fg\" })\n  }\n  return parts\n}\n\nfunction Cursor() {\n  return (\n    <span\n      aria-hidden\n      data-slot=\"terminal-cursor\"\n      className=\"ml-px inline-block h-[1.1em] w-[0.58em] translate-y-[0.2em] rounded-[1px] bg-fg animate-[caret-blink_1.1s_steps(1,end)_infinite]\"\n    />\n  )\n}\n\nfunction CommandRow({ text, typed, prompt, state }: { text: string; typed: number; prompt: string; state: \"hidden\" | \"typing\" | \"shown\" }) {\n  const parts = tokenize(text)\n  const shown = state === \"shown\" ? text.length : typed\n  let offset = 0\n  let cursorPlaced = state !== \"typing\"\n  return (\n    <div data-slot=\"terminal-line\" data-kind=\"command\" className={cn(\"flex gap-2\", state === \"hidden\" && \"invisible\")}>\n      <span className=\"shrink-0 text-accent-fg select-none\">{prompt}</span>\n      <span className=\"min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]\">\n        {parts.map((p, i) => {\n          const start = offset\n          offset += p.t.length\n          const vis = Math.max(0, Math.min(p.t.length, shown - start))\n          const withCursor = !cursorPlaced && shown <= offset\n          if (withCursor) cursorPlaced = true\n          return (\n            <span key={i} className={p.c}>\n              {p.t.slice(0, vis)}\n              {withCursor ? <Cursor /> : null}\n              <span className=\"invisible\">{p.t.slice(vis)}</span>\n            </span>\n          )\n        })}\n      </span>\n    </div>\n  )\n}\n\nfunction OutputRow({ line, hidden }: { line: TerminalLine; hidden: boolean }) {\n  const kind = line.kind ?? \"output\"\n  const glyph = GLYPH[kind] ?? GLYPH.output\n  return (\n    <div data-slot=\"terminal-line\" data-kind={kind} className={cn(\"flex gap-2\", hidden && \"invisible\")}>\n      <span className={cn(\"w-[1ch] shrink-0 select-none\", glyph.className)}>{glyph.mark}</span>\n      <span className={cn(\"min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]\", LINE_TEXT[kind] ?? LINE_TEXT.output)}>{line.text}</span>\n    </div>\n  )\n}\n\nfunction Terminal({\n  lines = SAMPLE_LINES,\n  title = \"Terminal\",\n  prompt = \"❯\",\n  typingSpeed = 26,\n  lineDelay = 160,\n  commandDelay = 600,\n  loop = false,\n  replay = true,\n  appearance = \"dark\",\n  onComplete,\n  className,\n}: TerminalProps) {\n  const reduce = useReducedMotion()\n  const ref = React.useRef<HTMLDivElement>(null)\n  const inView = useInView(ref, { amount: 0.3 })\n  const [pos, setPos] = React.useState<Pos>({ line: 0, char: 0 })\n  const done = pos.line >= lines.length\n  const allShown = Boolean(reduce) || done\n\n  React.useEffect(() => {\n    if (reduce || !inView) return\n    if (done) {\n      if (!loop) return\n      const t = setTimeout(() => setPos({ line: 0, char: 0 }), 2400)\n      return () => clearTimeout(t)\n    }\n    const cur = lines[pos.line]\n    const isCommand = cur.kind === \"command\"\n    let wait: number\n    let next: Pos\n    if (isCommand && pos.char < cur.text.length) {\n      wait = pos.char === 0 ? (cur.delay ?? commandDelay) : typingSpeed * jitter(pos.char)\n      next = { line: pos.line, char: pos.char + 1 }\n    } else {\n      // A finished command pauses as if Enter were pressed; output lines wait their delay.\n      wait = isCommand ? 280 : (cur.delay ?? lineDelay)\n      next = { line: pos.line + 1, char: 0 }\n    }\n    const t = setTimeout(() => setPos(next), wait)\n    return () => clearTimeout(t)\n  }, [pos, inView, reduce, done, lines, loop, typingSpeed, lineDelay, commandDelay])\n\n  const completeRef = React.useRef(onComplete)\n  React.useEffect(() => {\n    completeRef.current = onComplete\n  }, [onComplete])\n  React.useEffect(() => {\n    if (allShown) completeRef.current?.()\n  }, [allShown])\n\n  const transcript = lines.map((l) => (l.kind === \"command\" ? `${prompt} ${l.text}` : l.text)).join(\"\\n\")\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"terminal\"\n      data-tier=\"premium\"\n      // A screen is not a material surface: the forced dark window stays hairline inside glass, metal and paper.\n      data-material={appearance === \"dark\" ? \"hairline\" : undefined}\n      className={cn(\n        appearance === \"dark\" && \"dark\",\n        \"mx-auto w-full max-w-2xl overflow-hidden rounded-xl border border-border bg-bg text-fg shadow-raised\",\n        className\n      )}\n    >\n      <div data-slot=\"terminal-bar\" className=\"grid h-10 grid-cols-[1fr_auto_1fr] items-center border-b border-border bg-surface px-3\">\n        <span aria-hidden className=\"flex items-center gap-1.5 pl-1\">\n          <span className=\"size-2.5 rounded-full bg-danger/80\" />\n          <span className=\"size-2.5 rounded-full bg-warning/80\" />\n          <span className=\"size-2.5 rounded-full bg-success/80\" />\n        </span>\n        <span className=\"truncate px-2 text-xs text-fg-muted\">{title}</span>\n        <span className=\"flex justify-end\">\n          {replay ? (\n            <button\n              type=\"button\"\n              aria-label=\"Replay\"\n              onClick={() => setPos({ line: 0, char: 0 })}\n              className=\"grid size-7 place-items-center rounded-md text-fg-subtle outline-none transition-colors duration-[140ms] hover:bg-sunken hover:text-fg focus-visible:ring-2 focus-visible:ring-accent\"\n            >\n              <RotateCcwIcon className=\"size-3.5\" />\n            </button>\n          ) : null}\n        </span>\n      </div>\n      <div aria-hidden data-slot=\"terminal-body\" className=\"p-4 font-mono text-[12.5px] leading-[1.75] tabular-nums\">\n        {lines.map((l, i) => {\n          const state = allShown || i < pos.line ? \"shown\" : i === pos.line ? \"typing\" : \"hidden\"\n          return l.kind === \"command\" ? (\n            <CommandRow key={i} text={l.text} typed={pos.char} prompt={prompt} state={state} />\n          ) : (\n            <OutputRow key={i} line={l} hidden={state !== \"shown\"} />\n          )\n        })}\n        <div data-slot=\"terminal-line\" data-kind=\"idle\" className={cn(\"flex gap-2\", !allShown && \"invisible\")}>\n          <span className=\"shrink-0 text-accent-fg select-none\">{prompt}</span>\n          <span>{allShown ? <Cursor /> : null}</span>\n        </div>\n      </div>\n      <pre className=\"sr-only\">{transcript}</pre>\n    </div>\n  )\n}\n\nexport { Terminal }\nexport type { TerminalProps, TerminalLine }\n"
    }
  ]
}
