{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/embedded-terminal",
  "title": "Embedded Terminal",
  "description": "Embedded PTY terminal panel (requires node-pty peer)",
  "dependencies": [
    "ink",
    "strip-ansi"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/terminal-style.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/embedded-terminal.tsx",
      "content": "import { useIsScreenReaderEnabled, useStdout, Box, Text } from \"ink\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport stripAnsi from \"strip-ansi\";\n\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport { resolveBorderStyle } from \"@/lib/terminal-style\";\n\ninterface IPty {\n  kill: () => void;\n  onData: (cb: (data: string) => void) => void;\n  onExit: (cb: (e: { exitCode: number }) => void) => void;\n  resize: (columns: number, rows: number) => void;\n}\n\ninterface NodePtyModule {\n  spawn: (\n    command: string,\n    args: string[],\n    options: { cols: number; cwd?: string; name: string; rows: number }\n  ) => IPty;\n}\n\nexport interface EmbeddedTerminalProps {\n  command: string;\n  args?: string[];\n  cwd?: string;\n  width?: number;\n  height?: number;\n  onExit?: (code: number) => void;\n  isActive?: boolean;\n  \"aria-label\"?: string;\n}\n\nconst EMPTY_ARGS: string[] = [];\n\n/**\n * Renders a pseudo-terminal session inside the TUI.\n * Requires optional dependency `node-pty` (native build).\n */\nexport const EmbeddedTerminal = ({\n  command,\n  args = EMPTY_ARGS,\n  cwd,\n  width = 80,\n  height = 24,\n  onExit,\n  isActive = true,\n  \"aria-label\": ariaLabel,\n}: EmbeddedTerminalProps) => {\n  const unicode = useUnicode();\n  const isScreenReaderEnabled = useIsScreenReaderEnabled();\n  const { stdout } = useStdout();\n  const [raw, setRaw] = useState(\"\");\n  const [err, setErr] = useState<string | null>(null);\n  const [terminalColumns, setTerminalColumns] = useState(\n    () => stdout.columns ?? width\n  );\n  const pendingOutput = useRef(\"\");\n  const ptyRef = useRef<IPty | null>(null);\n  const onExitRef = useRef(onExit);\n  onExitRef.current = onExit;\n\n  useEffect(() => {\n    const updateColumns = () => setTerminalColumns(stdout.columns ?? width);\n    updateColumns();\n    stdout.on(\"resize\", updateColumns);\n    return () => {\n      stdout.off(\"resize\", updateColumns);\n    };\n  }, [stdout, width]);\n\n  const resolvedWidth = Math.max(\n    3,\n    Math.min(Math.max(3, Math.floor(width)), terminalColumns)\n  );\n  const ptyColumns = Math.max(\n    1,\n    resolvedWidth - (isScreenReaderEnabled ? 0 : 2)\n  );\n  const dimensionsRef = useRef({ columns: ptyColumns, rows: height });\n  dimensionsRef.current = { columns: ptyColumns, rows: height };\n\n  useEffect(() => {\n    if (!isActive) {\n      return;\n    }\n    let p: IPty | null = null;\n    let cancelled = false;\n\n    (async () => {\n      try {\n        const loadPty = new Function(\n          'return import(\"node-pty\")'\n        ) as () => Promise<NodePtyModule>;\n        const mod = await loadPty();\n        if (cancelled) {\n          return;\n        }\n        const dimensions = dimensionsRef.current;\n        const pty = mod.spawn(command, args, {\n          cols: dimensions.columns,\n          cwd,\n          name: \"xterm-color\",\n          rows: dimensions.rows,\n        });\n        p = pty;\n        ptyRef.current = pty;\n        pty.onData((d: string) => {\n          if (isScreenReaderEnabled) {\n            pendingOutput.current += d;\n            if (!d.includes(\"\\n\") && pendingOutput.current.length < 256) {\n              return;\n            }\n            const update = pendingOutput.current;\n            pendingOutput.current = \"\";\n            setRaw((previous) => (previous + update).slice(-500_000));\n          } else {\n            setRaw((previous) => (previous + d).slice(-500_000));\n          }\n        });\n        pty.onExit((e: { exitCode: number }) => {\n          onExitRef.current?.(e.exitCode);\n        });\n      } catch {\n        setErr(\n          \"Install optional peer: node-pty (native build required for your platform).\"\n        );\n      }\n    })();\n\n    return () => {\n      cancelled = true;\n      if (p) {\n        p.kill();\n      }\n      ptyRef.current = null;\n    };\n  }, [args, command, cwd, isActive, isScreenReaderEnabled]);\n\n  useEffect(() => {\n    ptyRef.current?.resize(ptyColumns, height);\n  }, [height, ptyColumns]);\n\n  const lines = useMemo(\n    () => stripAnsi(raw).split(\"\\n\").slice(-height),\n    [raw, height]\n  );\n\n  return (\n    <Box\n      flexDirection=\"column\"\n      borderStyle={resolveBorderStyle(\n        isScreenReaderEnabled ? undefined : \"round\",\n        unicode\n      )}\n      borderColor=\"cyan\"\n      width={resolvedWidth}\n      aria-role=\"list\"\n    >\n      <Text aria-label={ariaLabel ?? `Embedded terminal running ${command}`}>\n        {\"\"}\n      </Text>\n      {err ? (\n        <Text color=\"red\">{err}</Text>\n      ) : (\n        lines.map((line, i) => (\n          <Box key={i} aria-role=\"listitem\">\n            <Text>{line}</Text>\n          </Box>\n        ))\n      )}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "utility"
  ],
  "type": "registry:ui"
}