{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "type": "registry:ui",
  "title": "Code Block",
  "description": "A plug-and-play syntax-highlighted code block for documentation and developer interfaces.",
  "files": [
    {
      "path": "registry/sonaui/code-block/code-block.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  SiJavascript,\n  SiPython,\n  SiTypescript,\n} from \"@icons-pack/react-simple-icons\";\nimport { codeToHtml, type ShikiTransformer } from \"shiki\";\nimport { Check, Copy, Terminal } from \"lucide-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/sona-utils\";\n\nexport interface CodeBlockProps {\n  /** Source code to render. */\n  code: string;\n  /** Language identifier used for syntax highlighting. @default \"tsx\" */\n  language?: string;\n  /** Optional filename shown in the header. @default undefined */\n  filename?: string;\n  /** Shows a numbered gutter. @default false */\n  showLineNumbers?: boolean;\n  /** Lines to emphasize, as numbers or a comma-separated range. @default undefined */\n  highlightLines?: number[] | string;\n  /** Removes leading diff markers and styles added or removed lines. @default false */\n  showDiff?: boolean;\n  /** Lines to keep visually focused. @default undefined */\n  focusLines?: number[] | string;\n  /** Additional classes for the code block. @default undefined */\n  className?: string;\n}\n\nfunction parseLines(value: number[] | string | undefined) {\n  if (!value) return new Set<number>();\n  if (Array.isArray(value)) return new Set(value);\n  const lines = new Set<number>();\n  value.split(\",\").forEach((part) => {\n    const [start, end] = part.split(\"-\").map(Number);\n    if (!Number.isFinite(start)) return;\n    for (let line = start; line <= (Number.isFinite(end) ? end : start); line++)\n      lines.add(line);\n  });\n  return lines;\n}\n\nfunction languageIcon(language: string) {\n  const normalized = language.toLowerCase();\n  if ([\"ts\", \"tsx\", \"typescript\"].includes(normalized))\n    return <SiTypescript className=\"size-4\" />;\n  if ([\"js\", \"jsx\", \"javascript\"].includes(normalized))\n    return <SiJavascript className=\"size-4\" />;\n  if ([\"py\", \"python\"].includes(normalized))\n    return <SiPython className=\"size-4\" />;\n  if ([\"bash\", \"sh\", \"shell\"].includes(normalized))\n    return <Terminal className=\"size-4\" />;\n  return null;\n}\n\nfunction createLineTransformers(\n  code: string,\n  options: Pick<CodeBlockProps, \"highlightLines\" | \"focusLines\" | \"showDiff\">,\n): ShikiTransformer[] {\n  const highlighted = parseLines(options.highlightLines);\n  const focused = parseLines(options.focusLines);\n  const originalLines = code.split(\"\\n\");\n  return [\n    {\n      name: \"code-block-lines\",\n      preprocess(source) {\n        return options.showDiff\n          ? source.split(\"\\n\").map((line) => line.replace(/^[-+ ]/, \"\")).join(\"\\n\")\n          : source;\n      },\n      line(node, lineNumber) {\n        node.properties[\"data-line-number\"] = String(lineNumber);\n        if (highlighted.has(lineNumber)) node.properties[\"data-highlighted\"] = \"true\";\n        if (focused.has(lineNumber)) node.properties[\"data-focused\"] = \"true\";\n        if (options.showDiff) {\n          const original = originalLines[lineNumber - 1] ?? \"\";\n          if (original.startsWith(\"+\")) node.properties[\"data-diff\"] = \"added\";\n          if (original.startsWith(\"-\")) node.properties[\"data-diff\"] = \"removed\";\n        }\n      },\n    },\n  ];\n}\n\nexport default function CodeBlock({\n  code,\n  language = \"tsx\",\n  filename,\n  showLineNumbers = false,\n  highlightLines,\n  showDiff = false,\n  focusLines,\n  className,\n}: CodeBlockProps) {\n  const [copied, setCopied] = useState(false);\n  const emphasized = parseLines(highlightLines);\n  const focused = parseLines(focusLines);\n  const lines = useMemo(() => code.split(\"\\n\"), [code]);\n  const source = useMemo(() => {\n    const source = showDiff\n      ? lines.map((line) => line.replace(/^[-+ ]/, \"\")).join(\"\\n\")\n      : code;\n    return source;\n  }, [code, lines, showDiff]);\n  const [html, setHtml] = useState(\"\");\n  useEffect(() => {\n    let active = true;\n    void codeToHtml(source, { lang: language, themes: { light: \"github-light\", dark: \"github-dark\" }, defaultColor: \"light-dark()\", transformers: createLineTransformers(code, { highlightLines, focusLines, showDiff }) }).then((result) => {\n      if (active) setHtml(result.replace(/^<pre[^>]*><code>/, \"\").replace(/<\\/code><\\/pre>$/, \"\"));\n    });\n    return () => { active = false; };\n  }, [code, focusLines, highlightLines, language, showDiff, source]);\n\n  async function copyCode() {\n    await navigator.clipboard.writeText(\n      showDiff\n        ? lines.map((line) => line.replace(/^[-+ ]/, \"\")).join(\"\\n\")\n        : code,\n    );\n    setCopied(true);\n    window.setTimeout(() => setCopied(false), 1400);\n  }\n\n  return (\n    <div\n      className={cn(\n        \"relative w-full overflow-hidden rounded-xl border border-border/60 bg-card\",\n        className,\n      )}\n    >\n      {(filename || language) && (\n        <div className=\"flex h-11 items-center justify-between border-b border-border/60 px-4 text-xs text-muted-foreground\">\n          <span className=\"flex items-center gap-2\">\n            {languageIcon(language)}\n            {filename ?? language}\n          </span>\n          <button\n            type=\"button\"\n            onClick={copyCode}\n            className=\"rounded-md p-1.5 hover:bg-accent\"\n            aria-label=\"Copy code\"\n          >\n            {copied ? (\n              <Check className=\"size-4\" />\n            ) : (\n              <Copy className=\"size-4\" />\n            )}\n          </button>\n        </div>\n      )}\n      <div className=\"overflow-x-auto p-4 text-[.8125rem] leading-normal\">\n        <div\n          className={cn(\n            \"flex min-w-max\",\n            focusLines && \"[&_.line:not([data-focused])]:opacity-45\",\n          )}\n        >\n          {showLineNumbers && (\n            <div className=\"mr-4 select-none text-right text-muted-foreground\">\n              {lines.map((_, i) => (\n                <div key={i}>{i + 1}</div>\n              ))}\n            </div>\n          )}\n          <div\n            className=\"th-code-content block w-full whitespace-pre [&_.line]:inline-block [&_.line]:w-full [&_.line]:min-w-full [&_.th-line]:inline-block [&_.th-line]:w-full [&_.th-line]:min-w-full [&_.line[data-highlighted]]:bg-primary/10 [&_.line[data-diff=added]]:bg-green-500/10 [&_.line[data-diff=removed]]:bg-red-500/10 [&_.line[data-focused]]:font-medium\"\n            dangerouslySetInnerHTML={{ __html: html }}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "target": "components/ui/code-block/code-block.tsx"
    }
  ],
  "dependencies": [
    "shiki",
    "lucide-react",
    "@icons-pack/react-simple-icons"
  ],
  "registryDependencies": [
    "@sona-ui/sona-utils"
  ]
}