{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "terminal-output",
  "title": "Terminal Output",
  "description": "Streaming command output with stderr called out, an exit code, and scroll that follows without trapping you.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/terminal-output/terminal-output.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRight, Loader2 } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type TerminalStream = \"stdout\" | \"stderr\"\n\nexport type TerminalLine = {\n  text: string\n  stream?: TerminalStream\n}\n\nexport type TerminalOutputProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\"\n> & {\n  /** The command that produced this output, shown above it. */\n  command?: string\n  cwd?: string\n  /** A plain string is split on newlines and treated as stdout. */\n  output: string | readonly (TerminalLine | string)[]\n  running?: boolean\n  /** Shown once the command settles. Non-zero reads as a failure. */\n  exitCode?: number\n  maxHeight?: number | string\n  /** Keep the newest line in view while output arrives. */\n  follow?: boolean\n}\n\n// Written without a control character in the source so the pattern stays\n// readable and lint-clean. Covers the colour and cursor sequences a shell\n// emits; anything else is left alone rather than guessed at.\nconst ANSI = new RegExp(\"\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]\", \"g\")\n\nfunction toLines(\n  output: TerminalOutputProps[\"output\"]\n): readonly TerminalLine[] {\n  const raw =\n    typeof output === \"string\"\n      ? output\n          .replace(/\\n$/, \"\")\n          .split(\"\\n\")\n          .map((text) => ({ text }))\n      : output.map((line) => (typeof line === \"string\" ? { text: line } : line))\n\n  return raw.map((line) => ({ ...line, text: line.text.replace(ANSI, \"\") }))\n}\n\nexport function TerminalOutput({\n  command,\n  cwd,\n  output,\n  running = false,\n  exitCode,\n  maxHeight = \"18rem\",\n  follow = true,\n  className,\n  ...rootProps\n}: TerminalOutputProps) {\n  const lines = React.useMemo(() => toLines(output), [output])\n  const scroller = React.useRef<HTMLDivElement>(null)\n  const stuck = React.useRef(true)\n\n  React.useEffect(() => {\n    const element = scroller.current\n    if (!follow || !element || !stuck.current) return\n\n    element.scrollTop = element.scrollHeight\n  }, [lines, follow])\n\n  // Following is abandoned as soon as the reader scrolls up, and resumes when\n  // they come back to the bottom, so output never yanks the view away.\n  function onScroll(event: React.UIEvent<HTMLDivElement>) {\n    const { scrollTop, scrollHeight, clientHeight } = event.currentTarget\n    stuck.current = scrollHeight - scrollTop - clientHeight < 24\n  }\n\n  const settled = !running && exitCode !== undefined\n  const failed = settled && exitCode !== 0\n\n  return (\n    <div\n      data-slot=\"terminal-output\"\n      className={cn(\n        \"border-border bg-muted/40 min-w-0 overflow-hidden rounded-[calc(var(--radius)+0.15rem)] border\",\n        className\n      )}\n      {...rootProps}\n    >\n      {command ? (\n        <div\n          data-slot=\"terminal-output-command\"\n          className=\"border-border flex items-center gap-2 border-b px-3 py-1.5\"\n        >\n          <ChevronRight\n            aria-hidden=\"true\"\n            size={13}\n            className=\"text-primary shrink-0\"\n          />\n          <code className=\"text-foreground min-w-0 truncate font-[family-name:var(--font-mono),monospace] text-xs\">\n            {command}\n          </code>\n          {cwd ? (\n            <span className=\"text-muted-foreground ml-auto hidden shrink-0 truncate font-[family-name:var(--font-mono),monospace] text-[0.6875rem] sm:block\">\n              {cwd}\n            </span>\n          ) : null}\n        </div>\n      ) : null}\n\n      <div\n        ref={scroller}\n        onScroll={onScroll}\n        tabIndex={0}\n        role=\"log\"\n        aria-label=\"Command output\"\n        aria-busy={running}\n        className=\"overflow-auto px-3 py-2.5\"\n        style={{ maxHeight }}\n      >\n        <pre className=\"text-foreground m-0 rounded-none bg-transparent p-0 font-[family-name:var(--font-mono),monospace] text-xs leading-relaxed\">\n          <code>\n            {lines.map((line, index) => (\n              <span\n                key={index}\n                data-stream={line.stream ?? \"stdout\"}\n                className={cn(\n                  \"block break-words whitespace-pre-wrap\",\n                  line.stream === \"stderr\" && \"text-destructive\"\n                )}\n              >\n                {line.text === \"\" ? \" \" : line.text}\n              </span>\n            ))}\n          </code>\n        </pre>\n\n        {running ? (\n          <span className=\"text-muted-foreground mt-1 inline-flex items-center gap-1.5 text-xs\">\n            <Loader2\n              aria-hidden=\"true\"\n              size={12}\n              className=\"animate-spin motion-reduce:animate-none\"\n            />\n            Running\n          </span>\n        ) : null}\n      </div>\n\n      {settled ? (\n        <div\n          data-slot=\"terminal-output-status\"\n          data-failed={failed || undefined}\n          className={cn(\n            \"border-border border-t px-3 py-1.5 font-[family-name:var(--font-mono),monospace] text-[0.6875rem]\",\n            failed ? \"text-destructive\" : \"text-muted-foreground\"\n          )}\n        >\n          exit {exitCode}\n        </div>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai",
    "code",
    "terminal",
    "agent"
  ],
  "type": "registry:ui"
}