{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "type": "registry:ui",
  "title": "Code Block",
  "description": "Code-block chrome: filename header, language badge, copy button, optional line numbers. BYO highlighter.",
  "dependencies": [
    "@saasflare/ui"
  ],
  "files": [
    {
      "path": "components/ui/code-block.tsx",
      "type": "registry:ui",
      "content": "// @draft\n\"use client\"\n\n/**\n * @fileoverview Saasflare CodeBlock — framing UI for source-code blocks.\n * @author Saasflare™\n *\n * Provides the visual chrome (filename header, language badge, copy button,\n * optional line numbers, scrollable container) around code. Syntax\n * highlighting is intentionally NOT bundled — pass pre-highlighted HTML\n * via the `highlighted` prop (Shiki / Prism / Highlight.js are all heavy\n * and opinionated) or fall back to plain monospace via the `code` prop.\n *\n * @module packages/ui/components/ui/code-block\n * @package ui\n * @layer core\n *\n * @example\n * // Plain (no highlighting)\n * <CodeBlock code={`function hi() { return \"hello\" }`} language=\"ts\" filename=\"hi.ts\" />\n *\n * @example\n * // With pre-rendered HTML from Shiki on the server.\n * // NOTE: `highlighted` is injected via dangerouslySetInnerHTML — it MUST be\n * // trusted, server-generated markup. Never pass untrusted HTML (XSS sink).\n * <CodeBlock highlighted={await codeToHtml(src, { lang: \"ts\", theme: \"github-dark\" })} />\n */\n\nimport * as React from \"react\"\nimport { useMemo } from \"react\"\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\nimport { useClipboard } from \"@saasflare/ui\"\nimport { CheckIcon } from \"@saasflare/ui\"\n\n/** Props for the CodeBlock component. */\nexport interface CodeBlockProps\n    extends Omit<React.ComponentProps<\"div\">, keyof SaasflareComponentProps>,\n        SaasflareComponentProps {\n    /** Plain source code (rendered as monospace, no highlighting). */\n    code?: string\n    /**\n     * Pre-highlighted HTML — e.g. output from Shiki's `codeToHtml`. Injected\n     * verbatim via `dangerouslySetInnerHTML`, so it MUST be trusted,\n     * server-generated markup. Never pass untrusted/user-supplied HTML.\n     */\n    highlighted?: string\n    /** Language label shown in the header (purely cosmetic). */\n    language?: string\n    /** Filename shown left of the language badge. */\n    filename?: string\n    /**\n     * Show 1-based line numbers in the gutter. Only applies to the plain\n     * `code` path; when `highlighted` HTML is supplied it owns its own\n     * rendering and this flag is ignored.\n     */\n    showLineNumbers?: boolean\n    /** Hide the top bar (copy button still floats inside the block). */\n    hideHeader?: boolean\n    /** Hide the copy-to-clipboard button. */\n    hideCopyButton?: boolean\n}\n\nfunction CopyIcon(props: React.SVGProps<SVGSVGElement>) {\n    return (\n        <svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            aria-hidden=\"true\"\n            {...props}\n        >\n            <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n            <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n        </svg>\n    )\n}\n\n/**\n * Code block with filename header, language badge, copy button, and optional\n * line numbers. Highlighting is BYO (pass `highlighted` HTML or `code` text).\n *\n * @component\n * @layer core\n */\nexport function CodeBlock({\n    code,\n    highlighted,\n    language,\n    filename,\n    showLineNumbers = false,\n    hideHeader = false,\n    hideCopyButton = false,\n    className,\n    surface,\n    radius,\n    animated,\n    iconWeight,\n    ...props\n}: CodeBlockProps) {\n    const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n    const { copy, copied } = useClipboard()\n\n    const plainText = code ?? \"\"\n    const lines = useMemo(\n        () => (showLineNumbers && code ? code.split(\"\\n\") : null),\n        [code, showLineNumbers],\n    )\n\n    const onCopy = () => {\n        // Prefer plain `code` for clipboard; if only highlighted HTML is given,\n        // strip tags so users paste source, not markup.\n        const text =\n            code ?? (highlighted ? highlighted.replace(/<[^>]+>/g, \"\") : \"\")\n        copy(text)\n    }\n\n    const showHeader = !hideHeader && (filename || language)\n\n    return (\n        <div\n            {...props}\n            data-slot=\"code-block\"\n            data-surface={sf.surface}\n            data-radius={sf.radius}\n            data-animated={String(sf.animated)}\n            className={cn(\n                \"group relative overflow-hidden rounded-lg border bg-card text-card-foreground\",\n                className,\n            )}\n        >\n            {showHeader && (\n                <div\n                    data-slot=\"code-block-header\"\n                    className=\"flex items-center justify-between gap-2 border-b bg-muted/40 px-3 py-1.5 text-xs\"\n                >\n                    <div className=\"flex min-w-0 items-center gap-2\">\n                        {filename && (\n                            <span\n                                data-slot=\"code-block-filename\"\n                                className=\"truncate font-mono text-muted-foreground\"\n                            >\n                                {filename}\n                            </span>\n                        )}\n                        {language && (\n                            <span\n                                data-slot=\"code-block-language\"\n                                className=\"rounded-sm bg-primary/10 px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-wide text-muted-foreground\"\n                            >\n                                {language}\n                            </span>\n                        )}\n                    </div>\n                </div>\n            )}\n\n            <div className=\"relative\">\n                {!hideCopyButton && (\n                    <button\n                        type=\"button\"\n                        data-slot=\"code-block-copy\"\n                        onClick={onCopy}\n                        aria-label={copied ? \"Copied\" : \"Copy code\"}\n                        className={cn(\n                            \"absolute right-2 top-2 z-10 inline-flex size-7 items-center justify-center rounded-md border bg-background/80 text-muted-foreground shadow-xs backdrop-blur-sm\",\n                            \"transition-colors hover:text-foreground hover:bg-background\",\n                            \"opacity-0 group-hover:opacity-100 focus-visible:opacity-100\",\n                            \"[&_svg]:size-3.5\",\n                        )}\n                    >\n                        {copied ? <CheckIcon weight={sf.iconWeight} /> : <CopyIcon />}\n                    </button>\n                )}\n\n                {highlighted ? (\n                    <div\n                        data-slot=\"code-block-content\"\n                        className={cn(\n                            \"overflow-x-auto p-4 text-sm font-mono leading-relaxed\",\n                            \"[&_pre]:!bg-transparent [&_pre]:!p-0 [&_pre]:!m-0\",\n                        )}\n                        // eslint-disable-next-line react/no-danger\n                        dangerouslySetInnerHTML={{ __html: highlighted }}\n                    />\n                ) : lines ? (\n                    <pre\n                        data-slot=\"code-block-content\"\n                        className=\"overflow-x-auto p-4 text-sm font-mono leading-relaxed\"\n                    >\n                        <code>\n                            {lines.map((line, i) => (\n                                <span\n                                    key={`${i}:${line}`}\n                                    className=\"grid grid-cols-[auto_1fr] gap-3\"\n                                >\n                                    <span\n                                        data-slot=\"code-block-line-number\"\n                                        aria-hidden=\"true\"\n                                        className=\"select-none text-right text-muted-foreground/60 tabular-nums\"\n                                    >\n                                        {i + 1}\n                                    </span>\n                                    <span>{line || \" \"}</span>\n                                </span>\n                            ))}\n                        </code>\n                    </pre>\n                ) : (\n                    <pre\n                        data-slot=\"code-block-content\"\n                        className=\"overflow-x-auto p-4 text-sm font-mono leading-relaxed\"\n                    >\n                        <code>{plainText}</code>\n                    </pre>\n                )}\n            </div>\n        </div>\n    )\n}\n",
      "target": "components/ui/code-block.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/code-block.json"
  }
}
