{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal",
  "title": "Terminal",
  "description": "A terminal window that types its lines in view.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/terminal.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport { useInView, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst noopSubscribe = () => () => {};\n\ninterface TerminalProps {\n  /** Lines to type. Prefix with \"$ \" to render a prompt-colored command. */\n  lines: string[];\n  title?: string;\n  /** ms per character */\n  speed?: number;\n  className?: string;\n}\n\n/**\n * A terminal window that types its lines when scrolled into view.\n */\nexport function Terminal({\n  lines,\n  title = \"bash\",\n  speed = 18,\n  className,\n}: TerminalProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const inView = useInView(ref, { once: true, margin: \"0px 0px -15% 0px\" });\n  // useReducedMotion() already knows the preference on the first client\n  // render, but the server can't — keep the animated markup until hydrated.\n  const hydrated = useSyncExternalStore(noopSubscribe, () => true, () => false);\n  const reducedMotion = useReducedMotion() === true && hydrated;\n  const [progress, setProgress] = useState({ line: 0, char: 0 });\n\n  const done =\n    reducedMotion ||\n    (progress.line >= lines.length - 1 &&\n      progress.char >= (lines[lines.length - 1]?.length ?? 0));\n\n  useEffect(() => {\n    if (!inView || reducedMotion || done) return;\n    const current = lines[progress.line] ?? \"\";\n    const timeout = setTimeout(\n      () =>\n        setProgress((p) =>\n          p.char < current.length\n            ? { ...p, char: p.char + 1 }\n            : { line: p.line + 1, char: 0 }\n        ),\n      progress.char === 0 ? 260 : speed\n    );\n    return () => clearTimeout(timeout);\n  }, [inView, reducedMotion, done, progress, lines, speed]);\n\n  const visibleLines = reducedMotion\n    ? lines\n    : lines\n        .slice(0, progress.line + 1)\n        .map((l, i) => (i === progress.line ? l.slice(0, progress.char) : l));\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"terminal\"\n      className={cn(\n        \"relative w-full overflow-hidden rounded-xl border bg-neutral-950 font-mono text-sm shadow-xl\",\n        className\n      )}\n    >\n      <div className=\"flex items-center gap-3 border-b border-white/10 px-4 py-2.5\">\n        <div className=\"flex gap-1.5\">\n          <span className=\"size-3 rounded-full bg-red-500/80\" />\n          <span className=\"size-3 rounded-full bg-yellow-500/80\" />\n          <span className=\"size-3 rounded-full bg-green-500/80\" />\n        </div>\n        <span className=\"text-xs text-neutral-500\">{title}</span>\n      </div>\n      <div className=\"sr-only\">\n        {lines.map((line, i) => (\n          <p key={i}>{line}</p>\n        ))}\n      </div>\n      <div aria-hidden className=\"min-h-32 space-y-1.5 p-4 text-neutral-300\">\n        {visibleLines.map((line, i) => {\n          const isCommand = lines[i]?.startsWith(\"$ \");\n          const isLast = i === visibleLines.length - 1;\n          return (\n            <p key={i} className=\"leading-relaxed\">\n              {isCommand ? (\n                <>\n                  <span className=\"text-emerald-400\">$ </span>\n                  <span className=\"text-neutral-100\">{line.slice(2)}</span>\n                </>\n              ) : (\n                <span className=\"text-neutral-400\">{line}</span>\n              )}\n              {isLast && !done && (\n                <span className=\"text-neutral-100 motion-safe:animate-pulse\">▍</span>\n              )}\n            </p>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/terminal.tsx"
    }
  ],
  "type": "registry:ui"
}