{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "title": "Code Block",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A code panel with filename and language header, line numbers, highlighted lines, a wrap toggle, copy feedback and file tabs. No highlighter bundled; pass Shiki output as children.",
  "dependencies": [
    "lucide-react",
    "radix-ui"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/code-block.tsx",
      "content": "// Ballmac UI: Code Block. https://ui.ballmac.com/components/code-block\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Copy, FileCode2, WrapText } from \"lucide-react\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\n/* -------------------------------------------------------------------------------------------------\n * Shared pieces\n * -----------------------------------------------------------------------------------------------*/\n\nfunction CopyCodeButton({ getText, label }: { getText: () => string; label?: string }) {\n  const msg = useMessages()\n  label ??= msg(\"code-block.label\", \"Copy code\")\n  const [copied, setCopied] = React.useState(false)\n  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n  React.useEffect(() => () => clearTimeout(timer.current), [])\n  return (\n    <>\n      <button\n        type=\"button\"\n        data-slot=\"code-block-copy\"\n        aria-label={copied ? msg(\"code-block.copied\", \"Copied\") : label}\n        title={label}\n        onClick={async () => {\n          try {\n            await navigator.clipboard.writeText(getText())\n          } catch {\n            return\n          }\n          setCopied(true)\n          clearTimeout(timer.current)\n          timer.current = setTimeout(() => setCopied(false), 1800)\n        }}\n        className=\"flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n      >\n        {copied ? <Check aria-hidden=\"true\" className=\"size-3.5\" /> : <Copy aria-hidden=\"true\" className=\"size-3.5\" />}\n      </button>\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {copied ? \"Copied to clipboard\" : \"\"}\n      </span>\n    </>\n  )\n}\n\nfunction WrapToggle({ wrap, onWrapChange }: { wrap: boolean; onWrapChange: (wrap: boolean) => void }) {\n  const msg = useMessages()\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"code-block-wrap\"\n      aria-label={msg(\"code-block.wrapLines\", \"Wrap lines\")}\n      aria-pressed={wrap}\n      title={msg(\"code-block.wrapLines\", \"Wrap lines\")}\n      onClick={() => onWrapChange(!wrap)}\n      className=\"flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-pressed:bg-accent aria-pressed:text-foreground\"\n    >\n      <WrapText aria-hidden=\"true\" className=\"size-3.5\" />\n    </button>\n  )\n}\n\ntype CodeBodyProps = {\n  code?: string\n  children?: React.ReactNode\n  lineNumbers: boolean\n  highlight: number[]\n  wrap: boolean\n  label: string\n  codeRef: React.RefObject<HTMLDivElement | null>\n  className?: string\n}\n\n/**\n * Lines are `<span class=\"line\">`, the same structure Shiki emits, so line numbers (a CSS counter)\n * work for plain `code` and for pre-highlighted children alike.\n */\nfunction CodeBody({ code, children, lineNumbers, highlight, wrap, label, codeRef, className }: CodeBodyProps) {\n  const lines = React.useMemo(() => (code ?? \"\").replace(/\\n$/, \"\").split(\"\\n\"), [code])\n  const highlighted = React.useMemo(() => new Set(highlight), [highlight])\n  return (\n    <div\n      ref={codeRef}\n      data-slot=\"code-block-body\"\n      data-line-numbers={lineNumbers || undefined}\n      data-wrap={wrap || undefined}\n      tabIndex={0}\n      role=\"region\"\n      aria-label={label}\n      className={cn(\n        \"overflow-x-auto py-3 font-mono text-[13px] leading-6 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\",\n        lineNumbers &&\n          \"[counter-reset:line] [&_.line]:before:[counter-increment:line] [&_.line]:before:me-4 [&_.line]:before:inline-block [&_.line]:before:w-[3ch] [&_.line]:before:text-end [&_.line]:before:text-muted-foreground/60 [&_.line]:before:content-[counter(line)] [&_.line]:before:select-none\",\n        // Pre-highlighted children (e.g. Shiki's <pre>) keep their colors but lose their own box.\n        \"[&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:p-0 [&_pre]:font-mono\",\n        wrap ? \"[&_pre]:whitespace-pre-wrap [&_pre]:break-words\" : \"[&_pre]:whitespace-pre\",\n        className\n      )}\n    >\n      {code !== undefined ? (\n        <pre dir=\"ltr\" className={cn(\"min-w-full\", wrap ? \"w-full\" : \"w-max\")}>\n          <code className=\"block\">\n            {lines.map((line, i) => (\n              <span\n                key={i}\n                data-highlighted={highlighted.has(i + 1) || undefined}\n                className=\"line block px-4 data-[highlighted]:bg-accent data-[highlighted]:shadow-[inset_2px_0_0_0_var(--ring)]\"\n              >\n                {line || \" \"}\n              </span>\n            ))}\n          </code>\n        </pre>\n      ) : (\n        <div className=\"px-4\">{children}</div>\n      )}\n    </div>\n  )\n}\n\nconst headerClass = \"flex h-10 min-w-0 items-center gap-2 border-b bg-muted/40 pe-1.5 ps-3.5\"\n\n/* -------------------------------------------------------------------------------------------------\n * CodeBlock\n * -----------------------------------------------------------------------------------------------*/\n\ntype CodeBlockProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Source as plain text. Rendered unhighlighted in monospace; required for `highlight`. */\n  code?: string\n  /** Pre-highlighted markup (e.g. Shiki's output as JSX) used instead of `code`. Pass `code` too so copy gets clean text. */\n  children?: React.ReactNode\n  /** Shown in the header, e.g. \"app/page.tsx\". */\n  filename?: string\n  /** Language label shown in the header, e.g. \"tsx\". */\n  language?: string\n  /** Show line numbers. */\n  lineNumbers?: boolean\n  /** 1-based line numbers to highlight (plain `code` only). */\n  highlight?: number[]\n  /** Wrap long lines instead of scrolling horizontally (initial value when the toggle is shown). */\n  wrap?: boolean\n  /** Show a button that toggles line wrapping. */\n  wrapToggle?: boolean\n  /** Show the copy button. */\n  copyable?: boolean\n  /** Classes for the scrolling body, e.g. a max height. */\n  bodyClassName?: string\n}\n\nfunction CodeBlock({\n  code,\n  children,\n  filename,\n  language,\n  lineNumbers = false,\n  highlight = [],\n  wrap: wrapProp = false,\n  wrapToggle = false,\n  copyable = true,\n  bodyClassName,\n  className,\n  ...props\n}: CodeBlockProps) {\n  const [wrap, setWrap] = React.useState(wrapProp)\n  React.useEffect(() => setWrap(wrapProp), [wrapProp])\n  const codeRef = React.useRef<HTMLDivElement>(null)\n  const getText = () => code ?? codeRef.current?.textContent ?? \"\"\n  const hasHeader = Boolean(filename || language)\n\n  return (\n    <div\n      data-slot=\"code-block\"\n      className={cn(\"group/code relative w-full min-w-0 overflow-hidden rounded-xl border bg-card text-card-foreground\", className)}\n      {...props}\n    >\n      {hasHeader ? (\n        <div data-slot=\"code-block-header\" className={headerClass}>\n          {filename ? (\n            <>\n              <FileCode2 aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-muted-foreground\" />\n              <span className=\"truncate font-mono text-xs text-foreground\">{filename}</span>\n            </>\n          ) : null}\n          {language ? (\n            <span className=\"ms-auto shrink-0 font-mono text-[11px] tracking-wide text-muted-foreground uppercase\">{language}</span>\n          ) : null}\n          <span className={cn(\"flex shrink-0 items-center\", !language && \"ms-auto\")}>\n            {wrapToggle ? <WrapToggle wrap={wrap} onWrapChange={setWrap} /> : null}\n            {copyable ? <CopyCodeButton getText={getText} /> : null}\n          </span>\n        </div>\n      ) : wrapToggle || copyable ? (\n        <div className=\"absolute top-1.5 end-1.5 z-10 flex items-center rounded-md bg-card/90\">\n          {wrapToggle ? <WrapToggle wrap={wrap} onWrapChange={setWrap} /> : null}\n          {copyable ? <CopyCodeButton getText={getText} /> : null}\n        </div>\n      ) : null}\n      <CodeBody\n        code={children == null ? (code ?? \"\") : undefined}\n        lineNumbers={lineNumbers}\n        highlight={highlight}\n        wrap={wrap}\n        label={filename ?? (language ? `${language} code` : \"Code\")}\n        codeRef={codeRef}\n        className={cn(!hasHeader && (copyable || wrapToggle) && \"pe-10\", bodyClassName)}\n      >\n        {children}\n      </CodeBody>\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------------------------------\n * CodeBlockTabs\n * -----------------------------------------------------------------------------------------------*/\n\ntype CodeBlockFile = {\n  /** Tab label and region name, e.g. \"components/ballmac/button.tsx\". */\n  filename: string\n  /** Plain source. */\n  code: string\n  /** Language label for this file. */\n  language?: string\n  /** 1-based lines to highlight. */\n  highlight?: number[]\n  /** Pre-highlighted markup for this file (copy still uses `code`). */\n  children?: React.ReactNode\n}\n\ntype CodeBlockTabsProps = Omit<React.ComponentProps<\"div\">, \"children\" | \"defaultValue\" | \"dir\"> & {\n  /** One tab per file. */\n  files: CodeBlockFile[]\n  /** Filename of the tab open at first. */\n  defaultValue?: string\n  /** Controlled open tab (a filename). */\n  value?: string\n  /** Called with the filename of the newly selected tab. */\n  onValueChange?: (filename: string) => void\n  /** Show line numbers. */\n  lineNumbers?: boolean\n  /** Wrap long lines instead of scrolling horizontally. */\n  wrap?: boolean\n  /** Show the copy button (copies the open file). */\n  copyable?: boolean\n  /** Classes for each scrolling body. */\n  bodyClassName?: string\n}\n\nfunction CodeBlockTabs({\n  files,\n  defaultValue,\n  value: valueProp,\n  onValueChange,\n  lineNumbers = false,\n  wrap = false,\n  copyable = true,\n  bodyClassName,\n  className,\n  ...props\n}: CodeBlockTabsProps) {\n  const msg = useMessages()\n  const [internal, setInternal] = React.useState(defaultValue ?? files[0]?.filename ?? \"\")\n  const value = valueProp ?? internal\n  const current = files.find((f) => f.filename === value) ?? files[0]\n  const codeRef = React.useRef<HTMLDivElement>(null)\n\n  return (\n    <TabsPrimitive.Root\n      data-slot=\"code-block-tabs\"\n      value={current?.filename}\n      onValueChange={(next) => {\n        if (valueProp === undefined) setInternal(next)\n        onValueChange?.(next)\n      }}\n      className={cn(\"w-full min-w-0 overflow-hidden rounded-xl border bg-card text-card-foreground\", className)}\n      {...props}\n    >\n      <div className={cn(headerClass, \"ps-1.5\")}>\n        <TabsPrimitive.List\n          aria-label={msg(\"code-block.files\", \"Files\")}\n          className=\"flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto [scrollbar-width:none]\"\n        >\n          {files.map((file) => (\n            <TabsPrimitive.Trigger\n              key={file.filename}\n              value={file.filename}\n              className=\"relative h-10 shrink-0 px-2.5 font-mono text-xs text-muted-foreground outline-none transition-colors duration-150 after:absolute after:inset-x-2.5 after:bottom-0 after:h-px after:bg-transparent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset data-[state=active]:text-foreground data-[state=active]:after:bg-foreground\"\n            >\n              {file.filename.split(\"/\").pop()}\n            </TabsPrimitive.Trigger>\n          ))}\n        </TabsPrimitive.List>\n        {current?.language ? (\n          <span className=\"shrink-0 font-mono text-[11px] tracking-wide text-muted-foreground uppercase\">{current.language}</span>\n        ) : null}\n        {copyable ? <CopyCodeButton getText={() => current?.code ?? codeRef.current?.textContent ?? \"\"} /> : null}\n      </div>\n      {files.map((file) => (\n        <TabsPrimitive.Content key={file.filename} value={file.filename} tabIndex={-1} className=\"outline-none\">\n          <CodeBody\n            code={file.children == null ? file.code : undefined}\n            lineNumbers={lineNumbers}\n            highlight={file.highlight ?? []}\n            wrap={wrap}\n            label={file.filename}\n            codeRef={codeRef}\n            className={bodyClassName}\n          >\n            {file.children}\n          </CodeBody>\n        </TabsPrimitive.Content>\n      ))}\n    </TabsPrimitive.Root>\n  )\n}\n\nexport { CodeBlock, CodeBlockTabs, type CodeBlockProps, type CodeBlockTabsProps, type CodeBlockFile }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/code-block.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "code",
      "snippet",
      "syntax",
      "copy",
      "shiki",
      "tabs",
      "developer",
      "docs"
    ],
    "version": "1.0.0",
    "updated": "2026-09-28",
    "ai": {
      "summary": "<CodeBlock code filename language lineNumbers highlight={[2,3]} /> renders plain monospace text. For syntax colors, highlight on the server (e.g. Shiki) and pass the result as children plus the raw code for copying. <CodeBlockTabs files={[…]}> shows several files.",
      "whenToUse": [
        "Code samples in docs, READMEs rendered on the web and blog posts",
        "Showing a changed file with specific lines highlighted",
        "Several related files (component, usage, config) in one panel with tabs"
      ],
      "whenNotToUse": [
        "One-line install commands for several package managers (use install-tabs)",
        "Terminal sessions with prompts and output (use terminal)",
        "Editable code (use a code editor)"
      ],
      "composesWith": [
        "install-tabs",
        "terminal",
        "ai-message"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Focuses the scrollable code region (arrow keys scroll long lines), then wrap and copy buttons"
        },
        {
          "keys": "← / →",
          "action": "In CodeBlockTabs, moves between file tabs"
        },
        {
          "keys": "—",
          "action": "Copying announces 'Copied to clipboard' through a polite live region"
        }
      ],
      "customization": [
        "code (plain text) or children (pre-highlighted markup). Line numbers use a CSS counter on .line spans, which is also what Shiki emits",
        "highlight: 1-based line numbers (plain code only; with Shiki use its notation transformers)",
        "wrap (initial) and wrapToggle; copyable={false} hides copy",
        "bodyClassName for a max height, e.g. 'max-h-80'",
        "Shiki: const html = await codeToHtml(code, { lang, themes }) then <CodeBlock code={code} language={lang}><div dangerouslySetInnerHTML={{ __html: html }} /></CodeBlock>"
      ]
    },
    "examples": [
      "code-block-demo",
      "code-block-tabs"
    ],
    "url": "https://ui.ballmac.com/components/code-block"
  },
  "categories": [
    "developer"
  ],
  "type": "registry:ui"
}