{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "type": "registry:ui",
  "title": "Code block",
  "description": "Monospace code with copy, and a collapsible JSON viewer mode.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@wibo/utils"
  ],
  "files": [
    {
      "path": "src/components/code-block.tsx",
      "type": "registry:ui",
      "content": "import { useEffect, useRef, useState, type ComponentProps, type ReactNode } from \"react\";\nimport { Check, ChevronRight, Copy } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ntype JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };\n\ninterface CodeBlockProps extends Omit<ComponentProps<\"div\">, \"title\"> {\n  /** Source text. Ignored when `json` is set. */\n  code?: string;\n  /** Render a collapsible JSON viewer for this value instead of plain text. */\n  json?: unknown;\n  /** Levels of the JSON tree open by default. */\n  expandDepth?: number;\n  /** Header label, for example a file name. */\n  title?: ReactNode;\n  /** Language tag shown in the header. */\n  language?: string;\n  copyable?: boolean;\n  copyLabel?: string;\n  copiedLabel?: string;\n  /** Wrap long lines instead of scrolling horizontally. */\n  wrap?: boolean;\n  /** Words of a folded JSON node's size (`3 keys`, `1 item`), in the app's language. */\n  countLabels?: JsonCountLabels;\n}\n\n/** The nouns a folded JSON object or array shows after its count. */\ninterface JsonCountLabels {\n  item: string;\n  items: string;\n  key: string;\n  keys: string;\n}\n\nconst defaultCountLabels: JsonCountLabels = { item: \"item\", items: \"items\", key: \"key\", keys: \"keys\" };\n\n/**\n * Monospace code or JSON with an optional header and copy button. Mastra\n * Studio's code frame: the canvas tone (one step below a card), a hairline\n * border, 14 px radius, 12 px mono text, the file name in mono in the header.\n */\nfunction CodeBlock({\n  code,\n  json,\n  expandDepth = 2,\n  title,\n  language,\n  copyable = true,\n  copyLabel = \"Copy\",\n  copiedLabel = \"Copied\",\n  wrap = false,\n  countLabels = defaultCountLabels,\n  className,\n  ...props\n}: CodeBlockProps) {\n  const isJson = json !== undefined;\n  const text = isJson ? JSON.stringify(json, null, 2) : (code ?? \"\");\n  const [copied, setCopied] = useState(false);\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n  useEffect(() => () => clearTimeout(timer.current), []);\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(text);\n      setCopied(true);\n      clearTimeout(timer.current);\n      timer.current = setTimeout(() => setCopied(false), 1500);\n    } catch {\n      setCopied(false);\n    }\n  };\n\n  const label = language ?? (isJson ? \"json\" : undefined);\n  const hasHeader = Boolean(title || label || copyable);\n\n  return (\n    <div\n      data-slot=\"code-block\"\n      className={cn(\"overflow-hidden rounded-xl border border-border bg-background text-sm\", className)}\n      {...props}\n    >\n      {hasHeader ? (\n        <div className=\"flex h-9 items-center gap-2 border-b border-border pr-2 pl-4\">\n          <div className=\"min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground\">{title}</div>\n          {label ? <span className=\"font-mono text-xs text-muted-foreground\">{label}</span> : null}\n          {copyable ? (\n            <button\n              type=\"button\"\n              onClick={copy}\n              aria-label={copied ? copiedLabel : copyLabel}\n              className=\"press-icon inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-foreground/4 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 data-[copied=true]:text-success\"\n              data-copied={copied || undefined}\n            >\n              {copied ? <Check aria-hidden=\"true\" className=\"size-3.5 animate-check-in\" /> : <Copy aria-hidden=\"true\" className=\"size-3.5 animate-fade-in\" />}\n            </button>\n          ) : null}\n          <span className=\"sr-only\" aria-live=\"polite\">\n            {copied ? copiedLabel : \"\"}\n          </span>\n        </div>\n      ) : null}\n      <div className=\"max-h-[32rem] overflow-auto px-4 py-3\">\n        {isJson ? (\n          <div className=\"font-mono text-xs leading-5\">\n            <JsonNode value={toJsonValue(json)} depth={0} expandDepth={expandDepth} countLabels={countLabels} last />\n          </div>\n        ) : (\n          <pre className={cn(\"font-mono text-xs leading-5 text-foreground\", wrap ? \"break-words whitespace-pre-wrap\" : \"whitespace-pre\")}>\n            <code>{text}</code>\n          </pre>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction toJsonValue(value: unknown): JsonValue {\n  return JSON.parse(JSON.stringify(value ?? null)) as JsonValue;\n}\n\nfunction JsonPrimitive({ value }: { value: string | number | boolean | null }) {\n  if (value === null) return <span className=\"text-muted-foreground\">null</span>;\n  if (typeof value === \"string\") return <span className=\"break-all text-success\">{JSON.stringify(value)}</span>;\n  if (typeof value === \"number\") return <span className=\"text-info\">{String(value)}</span>;\n  return <span className=\"text-accent\">{String(value)}</span>;\n}\n\ninterface JsonNodeProps {\n  name?: string;\n  value: JsonValue;\n  depth: number;\n  expandDepth: number;\n  countLabels: JsonCountLabels;\n  last: boolean;\n}\n\nfunction JsonNode({ name, value, depth, expandDepth, countLabels, last }: JsonNodeProps) {\n  const isArray = Array.isArray(value);\n  const isObject = value !== null && typeof value === \"object\";\n  const [open, setOpen] = useState(depth < expandDepth);\n  const comma = last ? null : <span className=\"text-muted-foreground\">,</span>;\n  const key =\n    name !== undefined ? (\n      <>\n        <span className=\"text-foreground\">{JSON.stringify(name)}</span>\n        <span className=\"text-muted-foreground\">: </span>\n      </>\n    ) : null;\n\n  if (!isObject) {\n    return (\n      <div className=\"pl-4\">\n        {key}\n        <JsonPrimitive value={value as string | number | boolean | null} />\n        {comma}\n      </div>\n    );\n  }\n\n  const entries: [string | undefined, JsonValue][] = isArray\n    ? (value as JsonValue[]).map((item) => [undefined, item])\n    : Object.entries(value as Record<string, JsonValue>);\n  const [openBracket, closeBracket] = isArray ? [\"[\", \"]\"] : [\"{\", \"}\"];\n  const count = entries.length;\n\n  if (count === 0) {\n    return (\n      <div className=\"pl-4\">\n        {key}\n        <span className=\"text-muted-foreground\">\n          {openBracket}\n          {closeBracket}\n        </span>\n        {comma}\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <button\n        type=\"button\"\n        aria-expanded={open}\n        onClick={() => setOpen((value) => !value)}\n        className=\"inline-flex items-center rounded-sm text-left outline-none hover:bg-foreground/4 focus-visible:ring-2 focus-visible:ring-ring/50\"\n      >\n        <ChevronRight\n          aria-hidden=\"true\"\n          className={cn(\"size-4 text-muted-foreground transition-transform duration-150\", open && \"rotate-90\")}\n        />\n        {key}\n        <span className=\"text-muted-foreground\">{openBracket}</span>\n        {open ? null : (\n          <span className=\"text-muted-foreground\">\n            {\" \"}\n            {count} {isArray ? (count === 1 ? countLabels.item : countLabels.items) : count === 1 ? countLabels.key : countLabels.keys} {closeBracket}\n            {last ? \"\" : \",\"}\n          </span>\n        )}\n      </button>\n      {open ? (\n        <>\n          <div className=\"ml-2 border-l border-border pl-2\">\n            {entries.map(([childName, child], index) => (\n              <JsonNode\n                key={childName ?? index}\n                name={childName}\n                value={child}\n                depth={depth + 1}\n                expandDepth={expandDepth}\n                countLabels={countLabels}\n                last={index === count - 1}\n              />\n            ))}\n          </div>\n          <div className=\"pl-4 text-muted-foreground\">\n            {closeBracket}\n            {last ? \"\" : \",\"}\n          </div>\n        </>\n      ) : null}\n    </div>\n  );\n}\n\nexport { CodeBlock };\nexport type { CodeBlockProps, JsonCountLabels, JsonValue };\n"
    }
  ]
}
