{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cli-output",
  "title": "CLI Output",
  "description": "Terminal-style output display with ANSI color support and controls",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/devtools/cli-output/components/elements/cli-output.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Copy, Trash2 } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface CliOutputLine {\n  id: string;\n  text: string;\n  timestamp?: Date;\n}\n\ninterface CliOutputProps {\n  output: CliOutputLine[] | string[];\n  prompt?: string;\n  autoScroll?: boolean;\n  maxLines?: number;\n  showTimestamps?: boolean;\n  showControls?: boolean;\n  onClear?: () => void;\n  className?: string;\n}\n\nconst ANSI_COLORS: Record<string, string> = {\n  \"30\": \"text-gray-900 dark:text-gray-100\",\n  \"31\": \"text-red-500\",\n  \"32\": \"text-green-500\",\n  \"33\": \"text-yellow-500\",\n  \"34\": \"text-blue-500\",\n  \"35\": \"text-purple-500\",\n  \"36\": \"text-cyan-500\",\n  \"37\": \"text-gray-300\",\n  \"90\": \"text-gray-500\",\n  \"91\": \"text-red-400\",\n  \"92\": \"text-green-400\",\n  \"93\": \"text-yellow-400\",\n  \"94\": \"text-blue-400\",\n  \"95\": \"text-purple-400\",\n  \"96\": \"text-cyan-400\",\n  \"97\": \"text-white\",\n};\n\nconst ANSI_BG_COLORS: Record<string, string> = {\n  \"40\": \"bg-gray-900\",\n  \"41\": \"bg-red-500\",\n  \"42\": \"bg-green-500\",\n  \"43\": \"bg-yellow-500\",\n  \"44\": \"bg-blue-500\",\n  \"45\": \"bg-purple-500\",\n  \"46\": \"bg-cyan-500\",\n  \"47\": \"bg-gray-300\",\n};\n\nconst ANSI_STYLES: Record<string, string> = {\n  \"1\": \"font-bold\",\n  \"2\": \"opacity-50\",\n  \"3\": \"italic\",\n  \"4\": \"underline\",\n  \"9\": \"line-through\",\n};\n\nfunction parseAnsi(text: string): React.ReactNode[] {\n  const parts: React.ReactNode[] = [];\n  const regex = /\\x1b\\[([0-9;]*)m/g;\n  let lastIndex = 0;\n  let currentClasses: string[] = [];\n  let match: RegExpExecArray | null;\n\n  while ((match = regex.exec(text)) !== null) {\n    if (match.index > lastIndex) {\n      const segment = text.slice(lastIndex, match.index);\n      if (segment) {\n        parts.push(\n          <span key={lastIndex} className={currentClasses.join(\" \")}>\n            {segment}\n          </span>,\n        );\n      }\n    }\n\n    const codes = match[1].split(\";\");\n    for (const code of codes) {\n      if (code === \"0\" || code === \"\") {\n        currentClasses = [];\n      } else if (ANSI_COLORS[code]) {\n        currentClasses = currentClasses.filter((c) => !c.startsWith(\"text-\"));\n        currentClasses.push(ANSI_COLORS[code]);\n      } else if (ANSI_BG_COLORS[code]) {\n        currentClasses = currentClasses.filter((c) => !c.startsWith(\"bg-\"));\n        currentClasses.push(ANSI_BG_COLORS[code]);\n      } else if (ANSI_STYLES[code]) {\n        currentClasses.push(ANSI_STYLES[code]);\n      }\n    }\n\n    lastIndex = match.index + match[0].length;\n  }\n\n  if (lastIndex < text.length) {\n    const remaining = text.slice(lastIndex);\n    if (remaining) {\n      parts.push(\n        <span key={lastIndex} className={currentClasses.join(\" \")}>\n          {remaining}\n        </span>,\n      );\n    }\n  }\n\n  return parts.length > 0 ? parts : [text];\n}\n\nexport function CliOutput({\n  output,\n  prompt = \"$\",\n  autoScroll = true,\n  maxLines,\n  showTimestamps = false,\n  showControls = true,\n  onClear,\n  className,\n}: CliOutputProps) {\n  const scrollRef = React.useRef<HTMLDivElement>(null);\n  const [copied, setCopied] = React.useState(false);\n\n  const lines: CliOutputLine[] = React.useMemo(() => {\n    const normalized = output.map((item, idx) => {\n      if (typeof item === \"string\") {\n        return { id: `line-${idx}`, text: item };\n      }\n      return item;\n    });\n    if (maxLines && normalized.length > maxLines) {\n      return normalized.slice(-maxLines);\n    }\n    return normalized;\n  }, [output, maxLines]);\n\n  React.useEffect(() => {\n    if (autoScroll && scrollRef.current) {\n      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;\n    }\n  }, [lines, autoScroll]);\n\n  const handleCopy = React.useCallback(async () => {\n    const text = lines\n      .map((l) => l.text.replace(/\\x1b\\[[0-9;]*m/g, \"\"))\n      .join(\"\\n\");\n    await navigator.clipboard.writeText(text);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 1500);\n  }, [lines]);\n\n  const formatTime = (date: Date) => {\n    return date.toLocaleTimeString(\"en-US\", {\n      hour12: false,\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      second: \"2-digit\",\n    });\n  };\n\n  return (\n    <div\n      data-slot=\"cli-output\"\n      className={cn(\n        \"flex flex-col rounded-lg border border-border bg-gray-950 text-gray-100 overflow-hidden\",\n        className,\n      )}\n    >\n      {showControls && (\n        <div className=\"flex items-center justify-between px-3 py-1.5 border-b border-border bg-gray-900\">\n          <div className=\"flex gap-1.5\">\n            <div className=\"w-3 h-3 rounded-full bg-red-500\" />\n            <div className=\"w-3 h-3 rounded-full bg-yellow-500\" />\n            <div className=\"w-3 h-3 rounded-full bg-green-500\" />\n          </div>\n          <div className=\"flex gap-1\">\n            <button\n              type=\"button\"\n              onClick={handleCopy}\n              className=\"p-1 text-gray-400 hover:text-white transition-colors\"\n              aria-label={copied ? \"Copied\" : \"Copy output\"}\n            >\n              <Copy className=\"w-3.5 h-3.5\" />\n              {copied && (\n                <span\n                  className=\"absolute text-xs text-green-400 -mt-4 -ml-2\"\n                  aria-hidden=\"true\"\n                >\n                  ✓\n                </span>\n              )}\n            </button>\n            {onClear && (\n              <button\n                type=\"button\"\n                onClick={onClear}\n                className=\"p-1 text-gray-400 hover:text-white transition-colors\"\n                aria-label=\"Clear output\"\n              >\n                <Trash2 className=\"w-3.5 h-3.5\" />\n              </button>\n            )}\n          </div>\n        </div>\n      )}\n\n      <div\n        ref={scrollRef}\n        role=\"log\"\n        aria-live=\"polite\"\n        aria-label=\"Terminal output\"\n        className=\"flex-1 overflow-auto p-3 font-mono text-sm\"\n      >\n        {lines.map((line) => (\n          <div key={line.id} className=\"flex gap-2 leading-relaxed\">\n            {showTimestamps && line.timestamp && (\n              <span className=\"text-gray-600 shrink-0\">\n                [{formatTime(line.timestamp)}]\n              </span>\n            )}\n            <span className=\"text-green-400 shrink-0\">{prompt}</span>\n            <span className=\"flex-1 whitespace-pre-wrap break-all\">\n              {parseAnsi(line.text)}\n            </span>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport type { CliOutputProps, CliOutputLine };\n",
      "type": "registry:component"
    }
  ],
  "docs": "ANSI color parsing, auto-scroll, max lines limit, copy/clear controls, timestamp display.",
  "categories": [
    "devtools"
  ],
  "type": "registry:ui"
}