{
  "name": "terminal",
  "type": "registry:ui",
  "files": [
    {
      "path": "components/ui/terminal.tsx",
      "content": "\"use client\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst KEY_SOUNDS_DOWN: Record<string, [number, number]> = {\n  A: [31542, 85],\n  B: [40621, 107],\n  C: [39632, 95],\n  D: [32492, 85],\n  E: [23317, 83],\n  F: [32973, 87],\n  G: [33453, 94],\n  H: [33986, 93],\n  I: [25795, 91],\n  J: [34425, 88],\n  K: [34932, 90],\n  L: [35410, 95],\n  M: [41610, 93],\n  N: [41103, 90],\n  O: [26309, 84],\n  P: [26804, 83],\n  Q: [22245, 95],\n  R: [23817, 92],\n  S: [32031, 88],\n  T: [24297, 92],\n  U: [25313, 95],\n  V: [40136, 94],\n  W: [22790, 89],\n  X: [39148, 76],\n  Y: [24811, 93],\n  Z: [38694, 80],\n  \" \": [51541, 144],\n  \"-\": [42594, 90],\n  \"@\": [23317, 83],\n  \"/\": [42594, 90],\n  \".\": [42594, 90],\n  \":\": [42594, 90],\n  \"0\": [26309, 84],\n  \"1\": [25313, 95],\n  \"2\": [23317, 83],\n  \"3\": [23817, 92],\n  \"4\": [24297, 92],\n  \"5\": [24811, 93],\n  \"6\": [25313, 95],\n  \"7\": [25795, 91],\n  \"8\": [26309, 84],\n  \"9\": [26804, 83],\n  Enter: [19065, 110],\n};\n\nconst KEY_SOUNDS_UP: Record<string, [number, number]> = {\n  A: [31632, 80],\n  B: [40736, 95],\n  C: [39732, 85],\n  D: [32577, 80],\n  E: [23402, 80],\n  F: [33063, 80],\n  G: [33553, 85],\n  H: [34081, 85],\n  I: [25890, 85],\n  J: [34515, 85],\n  K: [35027, 85],\n  L: [35510, 85],\n  M: [41710, 85],\n  N: [41198, 85],\n  O: [26394, 80],\n  P: [26889, 80],\n  Q: [22345, 85],\n  R: [23912, 85],\n  S: [32121, 80],\n  T: [24392, 85],\n  U: [25413, 85],\n  V: [40236, 85],\n  W: [22880, 85],\n  X: [39228, 70],\n  Y: [24911, 85],\n  Z: [38779, 75],\n  \" \": [51691, 130],\n  \"-\": [42689, 85],\n  \"@\": [23402, 80],\n  \"/\": [42689, 85],\n  \".\": [42689, 85],\n  \":\": [42689, 85],\n  \"0\": [26394, 80],\n  \"1\": [25413, 85],\n  \"2\": [23402, 80],\n  \"3\": [23912, 85],\n  \"4\": [24392, 85],\n  \"5\": [24911, 85],\n  \"6\": [25413, 85],\n  \"7\": [25890, 85],\n  \"8\": [26394, 80],\n  \"9\": [26889, 80],\n  Enter: [19180, 100],\n};\n\nfunction useAudio(enabled: boolean) {\n  const ctxRef = useRef<AudioContext | null>(null);\n  const bufferRef = useRef<AudioBuffer | null>(null);\n  const readyRef = useRef(false);\n\n  useEffect(() => {\n    readyRef.current = false;\n    if (!enabled) return;\n\n    const controller = new AbortController();\n    let context: AudioContext | null = null;\n    let started = false;\n\n    const removeListeners = () => {\n      window.removeEventListener(\"click\", initAudio);\n      window.removeEventListener(\"keydown\", initAudio);\n    };\n\n    // Keep automatic demos silent until the user clicks, taps, or presses a key.\n    async function initAudio() {\n      if (started) return;\n      started = true;\n      removeListeners();\n\n      try {\n        context = new AudioContext();\n        ctxRef.current = context;\n        // Resume during the user gesture, before the download completes.\n        void context.resume().catch(() => {});\n        const response = await fetch(\"/sounds/sound.ogg\", {\n          signal: controller.signal,\n        });\n        if (!response.ok || controller.signal.aborted) return;\n        const bytes = await response.arrayBuffer();\n        if (controller.signal.aborted) return;\n        const buffer = await context.decodeAudioData(bytes);\n        if (controller.signal.aborted) return;\n        bufferRef.current = buffer;\n        readyRef.current = true;\n      } catch {\n        // A failed download or an unmounted demo must not stop the animation.\n      }\n    }\n\n    window.addEventListener(\"click\", initAudio, { once: true });\n    window.addEventListener(\"keydown\", initAudio, { once: true });\n\n    return () => {\n      removeListeners();\n      controller.abort();\n      readyRef.current = false;\n      bufferRef.current = null;\n      ctxRef.current = null;\n      if (context) void context.close().catch(() => {});\n    };\n  }, [enabled]);\n\n  const playSound = (sound: [number, number] | undefined) => {\n    if (!readyRef.current || !ctxRef.current || !bufferRef.current || !sound)\n      return;\n    if (ctxRef.current.state === \"suspended\") ctxRef.current.resume();\n    const src = ctxRef.current.createBufferSource();\n    src.buffer = bufferRef.current;\n    src.connect(ctxRef.current.destination);\n    src.start(0, sound[0] / 1000, sound[1] / 1000);\n  };\n\n  const down = (key: string) =>\n    playSound(KEY_SOUNDS_DOWN[key.toUpperCase()] || KEY_SOUNDS_DOWN[key]);\n  const up = (key: string) =>\n    playSound(KEY_SOUNDS_UP[key.toUpperCase()] || KEY_SOUNDS_UP[key]);\n\n  return { down, up };\n}\n\nfunction useInView(ref: React.RefObject<HTMLElement | null>, once = true) {\n  const [inView, setInView] = useState(false);\n  const triggered = useRef(false);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || (once && triggered.current)) return;\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting && !triggered.current) {\n          setInView(true);\n          if (once) {\n            triggered.current = true;\n            observer.disconnect();\n          }\n        }\n      },\n      { threshold: 0.1 },\n    );\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref, once]);\n\n  return inView;\n}\n\ntype TokenType =\n  | \"command\"\n  | \"flag\"\n  | \"string\"\n  | \"number\"\n  | \"operator\"\n  | \"path\"\n  | \"variable\"\n  | \"comment\"\n  | \"default\";\n\ninterface Token {\n  type: TokenType;\n  value: string;\n}\n\nfunction tokenizeBash(text: string): Token[] {\n  const tokens: Token[] = [];\n  const words = text.split(/(\\s+)/);\n\n  let isFirstWord = true;\n\n  for (const word of words) {\n    if (/^\\s+$/.test(word)) {\n      tokens.push({ type: \"default\", value: word });\n      continue;\n    }\n\n    if (word.startsWith(\"#\")) {\n      tokens.push({ type: \"comment\", value: word });\n      continue;\n    }\n\n    if (word.startsWith(\"$\")) {\n      tokens.push({ type: \"variable\", value: word });\n      isFirstWord = false;\n      continue;\n    }\n\n    if (word.startsWith(\"--\") || word.startsWith(\"-\")) {\n      tokens.push({ type: \"flag\", value: word });\n      isFirstWord = false;\n      continue;\n    }\n\n    if (/^[\"'].*[\"']$/.test(word)) {\n      tokens.push({ type: \"string\", value: word });\n      isFirstWord = false;\n      continue;\n    }\n\n    if (/^\\d+$/.test(word)) {\n      tokens.push({ type: \"number\", value: word });\n      isFirstWord = false;\n      continue;\n    }\n\n    if (/^[|>&<]+$/.test(word)) {\n      tokens.push({ type: \"operator\", value: word });\n      isFirstWord = true;\n      continue;\n    }\n\n    if (word.includes(\"/\") || word.startsWith(\".\") || word.startsWith(\"~\")) {\n      tokens.push({ type: \"path\", value: word });\n      isFirstWord = false;\n      continue;\n    }\n\n    if (isFirstWord) {\n      tokens.push({ type: \"command\", value: word });\n      isFirstWord = false;\n      continue;\n    }\n\n    tokens.push({ type: \"default\", value: word });\n  }\n\n  return tokens;\n}\n\nconst tokenColors: Record<TokenType, string> = {\n  command: \"text-emerald-400\",\n  flag: \"text-sky-400\",\n  string: \"text-amber-300\",\n  number: \"text-purple-400\",\n  operator: \"text-red-400\",\n  path: \"text-cyan-300\",\n  variable: \"text-pink-400\",\n  comment: \"text-neutral-500\",\n  default: \"text-neutral-300\",\n};\n\nfunction SyntaxHighlightedText({ text }: { text: string }) {\n  const tokens = tokenizeBash(text);\n\n  return (\n    <>\n      {tokens.map((token, i) => (\n        <span key={i} className={tokenColors[token.type]}>\n          {token.value}\n        </span>\n      ))}\n    </>\n  );\n}\n\ninterface TerminalLine {\n  type: \"command\" | \"output\";\n  content: string;\n}\n\nexport interface TerminalProps {\n  commands: string[];\n  outputs?: Record<number, string[]>;\n  username?: string;\n  className?: string;\n  typingSpeed?: number;\n  delayBetweenCommands?: number;\n  initialDelay?: number;\n  enableSound?: boolean;\n}\n\nexport function Terminal({\n  commands = [\"npx shadcn@latest init\"],\n  outputs = {},\n  username = \"Manus-Macbook\",\n  className,\n  typingSpeed = 50,\n  delayBetweenCommands = 800,\n  initialDelay = 500,\n  enableSound = true,\n}: TerminalProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const inView = useInView(containerRef);\n  const { down, up } = useAudio(enableSound);\n\n  const [lines, setLines] = useState<TerminalLine[]>([]);\n  const [currentText, setCurrentText] = useState(\"\");\n  const [commandIdx, setCommandIdx] = useState(0);\n  const [charIdx, setCharIdx] = useState(0);\n  const [outputIdx, setOutputIdx] = useState(-1);\n  const [phase, setPhase] = useState<\n    \"idle\" | \"typing\" | \"executing\" | \"outputting\" | \"pausing\" | \"done\"\n  >(\"idle\");\n  const [cursorVisible, setCursorVisible] = useState(true);\n\n  const currentCommand = commands[commandIdx] || \"\";\n  const currentOutputs = useMemo(\n    () => outputs[commandIdx] || [],\n    [outputs, commandIdx],\n  );\n  const isLastCommand = commandIdx === commands.length - 1;\n\n  useEffect(() => {\n    if (!inView || phase !== \"idle\") return;\n    const t = setTimeout(() => setPhase(\"typing\"), initialDelay);\n    return () => clearTimeout(t);\n  }, [inView, phase, initialDelay]);\n\n  useEffect(() => {\n    if (phase !== \"typing\") return;\n\n    if (charIdx < currentCommand.length) {\n      const char = currentCommand[charIdx];\n      down(char);\n      const t = setTimeout(\n        () => {\n          up(char);\n          setCurrentText(currentCommand.slice(0, charIdx + 1));\n          setCharIdx((c) => c + 1);\n        },\n        typingSpeed + Math.random() * 30,\n      );\n      return () => clearTimeout(t);\n    } else {\n      down(\"Enter\");\n      const t = setTimeout(() => {\n        up(\"Enter\");\n        setPhase(\"executing\");\n      }, 80);\n      return () => clearTimeout(t);\n    }\n  }, [phase, charIdx, currentCommand, typingSpeed, down, up]);\n\n  useEffect(() => {\n    if (phase !== \"executing\") return;\n\n    setLines((prev) => [...prev, { type: \"command\", content: currentCommand }]);\n    setCurrentText(\"\");\n\n    if (currentOutputs.length > 0) {\n      setOutputIdx(0);\n      setPhase(\"outputting\");\n    } else if (isLastCommand) {\n      setPhase(\"done\");\n    } else {\n      setPhase(\"pausing\");\n    }\n  }, [phase, currentCommand, currentOutputs.length, isLastCommand]);\n\n  useEffect(() => {\n    if (phase !== \"outputting\") return;\n\n    if (outputIdx >= 0 && outputIdx < currentOutputs.length) {\n      const t = setTimeout(() => {\n        setLines((prev) => [\n          ...prev,\n          { type: \"output\", content: currentOutputs[outputIdx] },\n        ]);\n        setOutputIdx((i) => i + 1);\n      }, 150);\n      return () => clearTimeout(t);\n    } else if (outputIdx >= currentOutputs.length) {\n      const t = setTimeout(() => {\n        if (isLastCommand) {\n          setPhase(\"done\");\n        } else {\n          setPhase(\"pausing\");\n        }\n      }, 300);\n      return () => clearTimeout(t);\n    }\n  }, [phase, outputIdx, currentOutputs, isLastCommand]);\n\n  useEffect(() => {\n    if (phase !== \"pausing\") return;\n    const t = setTimeout(() => {\n      setCharIdx(0);\n      setOutputIdx(-1);\n      setCommandIdx((c) => c + 1);\n      setPhase(\"typing\");\n    }, delayBetweenCommands);\n    return () => clearTimeout(t);\n  }, [phase, delayBetweenCommands]);\n\n  useEffect(() => {\n    const interval = setInterval(() => setCursorVisible((v) => !v), 530);\n    return () => clearInterval(interval);\n  }, []);\n\n  useEffect(() => {\n    if (contentRef.current) {\n      contentRef.current.scrollTop = contentRef.current.scrollHeight;\n    }\n  }, [lines, phase]);\n\n  const prompt = (\n    <span className=\"text-neutral-500\">\n      <span className=\"text-sky-500\">{username}</span>\n      <span className=\"text-emerald-600\">:</span>\n      <span className=\"text-sky-400\">~</span>\n      <span className=\"text-neutral-500\">$</span>{\" \"}\n    </span>\n  );\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"mx-auto w-full max-w-xl px-4 font-mono text-xs\",\n        className,\n      )}\n    >\n      <div className=\"overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900 shadow-2xl\">\n        {/* Title Bar */}\n        <div className=\"flex items-center gap-2 bg-neutral-800 px-4 py-3\">\n          <div className=\"flex items-center gap-1.5\">\n            <div className=\"h-3 w-3 rounded-full bg-red-500 transition-colors hover:bg-red-600\" />\n            <div className=\"h-3 w-3 rounded-full bg-yellow-500 transition-colors hover:bg-yellow-600\" />\n            <div className=\"h-3 w-3 rounded-full bg-green-500 transition-colors hover:bg-green-600\" />\n          </div>\n          <div className=\"flex-1 text-center\">\n            <span className=\"truncate text-xs text-neutral-400\">\n              {username} — bash\n            </span>\n          </div>\n          <div className=\"w-[52px]\" />\n        </div>\n\n        {/* Terminal Content */}\n        <div\n          ref={contentRef}\n          className=\"no-visible-scrollbar h-80 overflow-y-auto p-4 font-mono\"\n        >\n          {lines.map((line, i) => (\n            <div key={i} className=\"leading-relaxed whitespace-pre-wrap\">\n              {line.type === \"command\" ? (\n                <span>\n                  {prompt}\n                  <SyntaxHighlightedText text={line.content} />\n                </span>\n              ) : (\n                <span className=\"text-neutral-400\">{line.content}</span>\n              )}\n            </div>\n          ))}\n\n          {phase === \"typing\" && (\n            <div className=\"leading-relaxed whitespace-pre-wrap\">\n              {prompt}\n              <SyntaxHighlightedText text={currentText} />\n              <span className=\"ml-0.5 inline-block h-4 w-2 bg-neutral-300 align-middle\" />\n            </div>\n          )}\n\n          {(phase === \"done\" ||\n            phase === \"pausing\" ||\n            phase === \"outputting\") && (\n            <div className=\"leading-relaxed whitespace-pre-wrap\">\n              {prompt}\n              <span\n                className={cn(\n                  \"inline-block h-4 w-2 bg-neutral-300 align-middle transition-opacity duration-100\",\n                  !cursorVisible && \"opacity-0\",\n                )}\n              />\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/terminal.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "Terminal"
}