{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "type": "registry:ui",
  "title": "Code Block",
  "description": "Fenced code block with a title bar (title left, language tag + copy button right). The copy action shells out to `navigator.clipboard.writeText` and flips an inline \"Copied!\" affordance for 1.5s — the only piece of state this primitive owns. Everything…",
  "author": "ofri-peretz <https://github.com/ofri-peretz>",
  "categories": [
    "blog",
    "primitive"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://ds.interlace.tools/r/theme.json",
    "https://ds.interlace.tools/r/cn.json",
    "https://ds.interlace.tools/r/skeleton.json"
  ],
  "files": [
    {
      "path": "registry/interlace-ui/code-block.tsx",
      "target": "components/ui/code-block.tsx",
      "type": "registry:ui",
      "content": "'use client';\n\nimport * as React from 'react';\n\n// @interlace/code-block v1.1.0 — Interlace design system.\n// Docs, props and live preview: https://ds.interlace.tools/c/code-block\n// What changed since: https://ds.interlace.tools/c/code-block#history\n// Generated banner — keep it, the upgrade diff reads this version.\n\n/**\n * @interlace/ui — CodeBlock\n *\n * Fenced code block with a title bar (title left, language tag + copy button\n * right). The copy action shells out to `navigator.clipboard.writeText` and\n * flips an inline \"Copied!\" affordance for 1.5s — the only piece of state\n * this primitive owns. Everything else is structural: a `<figure>` wrapping\n * a `<pre><code class=\"language-{lang}\">`, ready for any downstream syntax\n * highlighter (Shiki, Prism, hand-rolled — we don't care).\n *\n * The header is omitted entirely when neither `title` nor `language` are\n * provided AND copy is disabled, so the primitive degrades to a clean\n * `<figure><pre><code/></pre></figure>` for inline snippets.\n *\n * ## Anatomy\n *\n *   CodeBlock                          (figure — data-min-viewport=320)\n *     ├─ figcaption                    (header bar; rendered only when needed)\n *     │   ├─ {title}                   (left)\n *     │   ├─ {language tag}            (right)\n *     │   └─ <button> \"Copy\" / \"Copied!\" (right; client-only)\n *     └─ <pre>\n *         └─ <code class=\"language-{lang}\">{children}</code>\n *\n * ## MIN_VIEWPORT — 320\n *\n * Code blocks are the load-bearing surface of a docs site and MUST work on\n * a 320 CSS-px phone. We never wrap content (would shred indentation) — we\n * `overflow-x-auto` instead, so a narrow viewport gets a horizontally\n * scrollable block rather than mangled syntax.\n *\n * | Rule | Concept                          | Where in this file                                          |\n * | ---- | -------------------------------- | ----------------------------------------------------------- |\n * | R4   | Extends native el                | `React.ComponentProps<'figure'> & CodeBlockProps`           |\n * | R6   | data-slot per part               | code-block / code-block-header / code-block-title / code-block-language / code-block-copy / code-block-pre / code-block-code |\n * | R7   | className merged + ...rest       | `cn(BASE, className)` + `{...props}`                        |\n * | R8   | No isXxx; props are scalars      | `title?` / `language?` only — no boolean variants           |\n * | R10  | Composition seam                 | `title` / `language` slots accept ReactNode                 |\n * | R14  | Declares min viewport            | `data-min-viewport={String(MIN_VIEWPORT)}` + exported const |\n * | R18  | Tailwind only                    | Zero inline `style`; utility classes only                   |\n * | R19  | Tokens only                      | `bg-card` / `border-border` / `rounded-md` / `p-md` / `text-code` |\n * | R20  | AA contrast                      | foreground on card surface (semantic tokens, AA-clean)      |\n * | R25  | Client component                 | Owns `useState` for the copy affordance                     |\n * | R26  | A11y                             | Copy button has accessible label + aria-live region for state |\n */\n\nimport { Check, Copy } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\nimport { Skeleton } from '@/components/ui/skeleton';\n\nexport const MIN_VIEWPORT = 320 as const;\n\nconst COPIED_RESET_MS = 1500;\n\ntype CodeBlockProps = Omit<React.ComponentProps<'figure'>, 'title' | 'children'> & {\n  /** Optional header title — usually a filename like `eslint.config.mjs`. */\n  title?: React.ReactNode;\n  /** Optional language tag — lowercases into `language-{lang}` on `<code>`. */\n  language?: string;\n  /**\n   * The fenced code source — a string, JSX, or pre-highlighted markup.\n   * Optional when `loading={true}` (the skeleton has no content to render).\n   */\n  children?: React.ReactNode;\n  /**\n   * When true, render a `<Skeleton variant=\"code-block\" />` (multi-line\n   * monospace silhouette) in place of the figure. Useful while a Shiki\n   * highlight or fetch resolves.\n   */\n  loading?: boolean;\n};\n\nconst CodeBlock = React.forwardRef<HTMLElement, CodeBlockProps>(\n  ({ className, title, language, children, loading, ...props }, ref) => {\n    // Hooks must run unconditionally per React rules — the loading\n    // early-return goes AFTER hook declarations so the call order is\n    // stable across renders when `loading` flips.\n    const [copied, setCopied] = React.useState(false);\n    const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n    // Clear the pending reset timer on unmount so we don't setState on a\n    // detached node (R25 — client component must clean up its own side-effects).\n    React.useEffect(() => {\n      return () => {\n        if (timerRef.current) clearTimeout(timerRef.current);\n      };\n    }, []);\n\n    const handleCopy = React.useCallback(async () => {\n      // Pull the raw text out of children. Strings copy directly; for nodes\n      // we fall back to the textContent of the rendered <code>. We capture\n      // it lazily here so a pre-highlighted JSX child still copies cleanly.\n      const text =\n        typeof children === 'string'\n          ? children\n          : (codeRef.current?.textContent ?? '');\n\n      try {\n        if (navigator?.clipboard?.writeText) {\n          await navigator.clipboard.writeText(text);\n        }\n        setCopied(true);\n        if (timerRef.current) clearTimeout(timerRef.current);\n        timerRef.current = setTimeout(() => setCopied(false), COPIED_RESET_MS);\n      } catch {\n        // Clipboard can reject in insecure contexts / sandboxed iframes. We\n        // intentionally swallow — the snippet is still visible and selectable.\n      }\n    }, [children]);\n\n    const codeRef = React.useRef<HTMLElement>(null);\n\n    // Loading early-return AFTER all hooks (useState, useEffect,\n    // useCallback, useRef) so hook order stays stable across renders.\n    if (loading) {\n      return (\n        <Skeleton\n          variant=\"code-block\"\n          data-slot=\"code-block\"\n          data-min-viewport={String(MIN_VIEWPORT)}\n          className={className}\n        />\n      );\n    }\n\n    const showHeader = Boolean(title) || Boolean(language) || true; // always show — copy button needs a home\n    const langClass = language ? `language-${language}` : undefined;\n\n    return (\n      <figure\n        ref={ref}\n        data-slot=\"code-block\"\n        data-min-viewport={String(MIN_VIEWPORT)}\n        data-language={language ?? undefined}\n        className={cn(\n          'bg-card border-border overflow-hidden rounded-md border',\n          className,\n        )}\n        {...props}\n      >\n        {showHeader ? (\n          <figcaption\n            data-slot=\"code-block-header\"\n            className={cn(\n              'border-border flex items-center justify-between gap-sm border-b px-md py-xs',\n              'text-ui-sm text-muted-foreground',\n            )}\n          >\n            <span\n              data-slot=\"code-block-title\"\n              className=\"min-w-0 truncate font-medium text-foreground\"\n            >\n              {title}\n            </span>\n            <div className=\"flex items-center gap-sm\">\n              {language ? (\n                <span\n                  data-slot=\"code-block-language\"\n                  className=\"font-mono uppercase tracking-wide\"\n                >\n                  {language}\n                </span>\n              ) : null}\n              <button\n                type=\"button\"\n                data-slot=\"code-block-copy\"\n                onClick={handleCopy}\n                aria-label={copied ? 'Copied to clipboard' : 'Copy code to clipboard'}\n                className={cn(\n                  'inline-flex items-center gap-xs rounded-md px-xs py-xs',\n                  'text-ui-sm text-muted-foreground hover:text-foreground',\n                  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n                  'transition-colors',\n                )}\n              >\n                {copied ? (\n                  <Check aria-hidden className=\"size-4\" />\n                ) : (\n                  <Copy aria-hidden className=\"size-4\" />\n                )}\n                <span aria-live=\"polite\">{copied ? 'Copied!' : 'Copy'}</span>\n              </button>\n            </div>\n          </figcaption>\n        ) : null}\n        <pre\n          data-slot=\"code-block-pre\"\n          // tabIndex=0 keeps overflow scroll keyboard-reachable per axe\n          // `scrollable-region-focusable` (WCAG 2.1.1). On narrow viewports\n          // this lets keyboard users scroll the snippet sideways with arrow\n          // keys; the focus-visible ring is the standard DS contract.\n          tabIndex={0}\n          className={cn(\n            'overflow-x-auto p-md',\n            'text-code font-mono text-foreground',\n            'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',\n          )}\n        >\n          <code\n            ref={codeRef}\n            data-slot=\"code-block-code\"\n            className={langClass}\n          >\n            {children}\n          </code>\n        </pre>\n      </figure>\n    );\n  },\n);\nCodeBlock.displayName = 'CodeBlock';\n\nexport { CodeBlock };\nexport type { CodeBlockProps };\n"
    }
  ],
  "meta": {
    "tier": "primitive",
    "client": true,
    "minViewport": 320,
    "loading": true,
    "version": "1.1.0",
    "since": "1.0.0"
  },
  "docs": "## @interlace/code-block\n\nInstalled to `components/ui/code-block.tsx`.\n\n```tsx\nimport { /* … */ } from '@/components/ui/code-block';\n```\n\nProps, a11y contract, live preview and source: https://ds.interlace.tools/c/code-block\n\nRequires the `@interlace/theme` CSS baseline (installed automatically as a registry dependency)."
}
