{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "title": "Code block",
  "description": "Line-streaming code surface with typed syntax tokens, static output mode, and accessible copy feedback.",
  "registryDependencies": [
    "https://beadkit.dev/r/ai-core.json"
  ],
  "files": [
    {
      "path": "components/ai/CodeBlock.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useState, type CSSProperties } from \"react\";\nimport styles from \"./ai-state.module.css\";\nimport { useReducedMotion } from \"./useReducedMotion\";\n\nexport type CodeTokenTone = \"keyword\" | \"string\" | \"number\" | \"function\" | \"muted\" | \"plain\";\n\nexport interface CodeToken {\n  text: string;\n  tone?: CodeTokenTone;\n}\n\nexport interface CodeLine {\n  tokens: readonly CodeToken[];\n}\n\nexport interface CodeBlockProps {\n  filename: string;\n  language?: string;\n  lines: readonly CodeLine[];\n  rawCode: string;\n  /** Replay the line-by-line presentation. Set false, or use settled, for static output. */\n  autoplay?: boolean;\n  status?: \"streaming\" | \"settled\";\n  className?: string;\n}\n\nconst toneClasses: Record<CodeTokenTone, string> = {\n  keyword: \"text-foreground font-semibold\",\n  string: \"text-success\",\n  number: \"text-destructive\",\n  function: \"text-foreground\",\n  muted: \"text-muted-foreground\",\n  plain: \"text-muted-foreground\",\n};\n\nexport function CodeBlock({ filename, language, lines, rawCode, autoplay = true, status = \"streaming\", className = \"\" }: CodeBlockProps) {\n  const reducedMotion = useReducedMotion();\n  const animated = autoplay && status === \"streaming\" && !reducedMotion;\n  const [count, setCount] = useState(animated ? 0 : lines.length);\n  const [copyState, setCopyState] = useState<\"idle\" | \"copied\" | \"failed\">(\"idle\");\n  const done = count >= lines.length;\n\n  useEffect(() => {\n    if (!animated) {\n      setCount(lines.length);\n      return;\n    }\n    const timer = window.setTimeout(() => setCount((current) => (current >= lines.length ? 0 : current + 1)), count === 0 ? 400 : done ? 3200 : 240);\n    return () => window.clearTimeout(timer);\n  }, [animated, count, done, lines.length]);\n\n  const copy = useCallback(async () => {\n    try {\n      if (!navigator.clipboard) throw new Error(\"Clipboard unavailable\");\n      await navigator.clipboard.writeText(rawCode);\n      setCopyState(\"copied\");\n      window.setTimeout(() => setCopyState(\"idle\"), 1500);\n    } catch {\n      setCopyState(\"failed\");\n      window.setTimeout(() => setCopyState(\"idle\"), 1500);\n    }\n  }, [rawCode]);\n\n  const copyLabel = copyState === \"copied\" ? \"Copied\" : copyState === \"failed\" ? \"Copy failed\" : \"Copy\";\n  return (\n    <div data-ai-state=\"code-block\" data-status={status} className={`bg-card w-full max-w-md overflow-hidden rounded-lg shadow-sm ${className}`}>\n      <div className=\"border-border flex items-center justify-between border-b px-3 py-2.5\">\n        <span className=\"flex min-w-0 items-baseline gap-2\"><span className=\"text-foreground truncate font-mono text-[12px] font-medium\">{filename}</span>{language ? <span className=\"text-muted-foreground text-[11.5px]\">{language}</span> : null}</span>\n        <button type=\"button\" aria-label={copyLabel} onClick={copy} className={`hover:bg-muted flex h-7 shrink-0 items-center gap-1 rounded-md px-1.5 text-[11.5px] font-medium transition-colors duration-100 ${copyState === \"copied\" ? \"text-success\" : copyState === \"failed\" ? \"text-destructive\" : \"text-muted-foreground hover:text-foreground\"}`}>\n          {copyState === \"copied\" ? \"✓\" : \"⧉\"} {copyLabel}\n        </button>\n      </div>\n      <pre aria-label={`${filename} source code`} className=\"bg-muted min-h-[137px] max-w-full overflow-x-auto px-3 py-2.5 font-mono text-[11.5px] leading-[1.7]\"><code>{lines.slice(0, count).map((line, index) => <span key={index} className={`${animated ? styles.fadeUp : \"\"} flex min-w-max`} style={animated ? ({ animationDelay: \"0ms\" } as CSSProperties) : undefined}><span aria-hidden=\"true\" className=\"text-muted-foreground w-5 shrink-0 select-none text-right text-[10.5px] leading-[1.86]\">{index + 1}</span><span className=\"pl-2.5 whitespace-pre\">{line.tokens.map((token, tokenIndex) => <span key={tokenIndex} className={toneClasses[token.tone ?? \"plain\"]}>{token.text}</span>)}{animated && index === count - 1 && !done ? <span aria-hidden=\"true\" className=\"bg-accent ml-0.5 inline-block h-3 w-[3px] translate-y-0.5 rounded-full\" /> : null}</span></span>)}</code></pre>\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">{copyState === \"failed\" ? \"Unable to copy code\" : copyState === \"copied\" ? \"Code copied\" : \"\"}</span>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai/CodeBlock.tsx"
    }
  ],
  "type": "registry:component"
}
