{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "title": "Code Block",
  "author": "@fma-ds/ds",
  "description": "A code block with shiki syntax highlighting, for docs and AI chat UIs. `<CodeBlock code={code} language=\"tsx\" />` is complete on its own.\n\n### Parts\n\n- **`CodeBlockHeader`**: the chrome — `CodeBlockTitle`, `CodeBlockLanguage`, `CodeBlockWrapToggle`, `CodeBlockCopyButton`, `CodeBlockDownloadButton`.\n- **`CodeBlockExpandButton`**: with `maxLines`.\n- **`CodeBlockContent`**: to place inside your own scroll container.\n- **`CodeBlockLineActions`**: one action group on the hovered or focused line.\n\nA pinned copy button stays put while the code scrolls.\n\n### Highlighting\n\nshiki loads lazily on the first highlight (JavaScript regex engine, no WebAssembly) through the `code-block-highlight` lib. Pass `lines` from `highlightCode()` in a server loader, or `highlight={false}`, to never load it. Light and dark token colors switch with the theme.\n\n### Lines\n\n- **Marking**: `showLineNumbers`/`startLine`, `highlightedLines`/`highlightedWords`/`focusedLines` (line specs like `\"2-4,7\"`), `diff` and `lineLevels` — or shiki `transformers`.\n- **`streaming`**: grows the code with a caret and stick-to-bottom, re-rendering only changed lines.\n- **`foldable`**: folds by indentation.\n- **`selectable`**: turns lines into a multi-select listbox.",
  "dependencies": [
    "cn@^0.5.0",
    "lucide-react@^1.61.0"
  ],
  "registryDependencies": [
    "@fma-ds/button",
    "@fma-ds/copy-button",
    "@fma-ds/code-block-highlight"
  ],
  "files": [
    {
      "path": "src/core/code-block/code-block.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { ChevronDownIcon, ChevronRightIcon, DownloadIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  CSSProperties,\n  KeyboardEvent as ReactKeyboardEvent,\n  ReactNode,\n} from \"react\";\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  memo,\n  useCallback,\n  useContext,\n  useDeferredValue,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/core/button/button\";\nimport { CopyButton } from \"@/core/copy-button/copy-button\";\nimport type {\n  CodeBlockDiffSpec,\n  CodeBlockLevelSpec,\n  CodeBlockLine,\n  CodeBlockLineActionsRender,\n  CodeBlockLineSpec,\n  CodeBlockThemes,\n  CodeBlockToken,\n  CodeBlockTransformer,\n  CodeBlockWordSpec,\n} from \"@/lib/code-block-highlight\";\nimport {\n  highlightCode,\n  markdownCodeProps,\n  markdownFences,\n  resolveCodeBlockLanguage,\n  stripNotationComments,\n  toPlainLines,\n} from \"@/lib/code-block-highlight\";\n\n// Bundlers replace process.env.NODE_ENV at build time; declared here so\n// consumers don't need @types/node just for this check.\ndeclare const process: { env: { NODE_ENV?: string } };\n\n/* -------------------------------------------------------------------------- */\n/*                                   Context                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Two contexts: the document changes on every streamed chunk, the config does\n * not. Splitting them keeps a stream from re-rendering the chrome per chunk.\n */\ntype CodeBlockConfigValue = {\n  language?: string;\n  resolvedLanguage?: string;\n  showLineNumbers: boolean;\n  wrap: boolean;\n  setWrap: (wrap: boolean) => void;\n  wrapControlled: boolean;\n  expanded: boolean;\n  setExpanded: (expanded: boolean) => void;\n  collapsible: boolean;\n  streaming: boolean;\n  contentId: string;\n};\n\ntype CodeBlockDocumentValue = {\n  code: string;\n  lines: CodeBlockLine[];\n  selected: Set<number>;\n  selectable: boolean;\n  toggleLine: (line: number, extend: boolean) => void;\n  clearSelection: () => void;\n  foldable: boolean;\n  foldRegions: CodeBlockFoldRegion[];\n  folded: Set<number>;\n  toggleFold: (start: number) => void;\n  setFolded: (folded: number[]) => void;\n};\n\nconst CodeBlockConfigContext = createContext<CodeBlockConfigValue | null>(null);\nconst CodeBlockDocumentContext = createContext<CodeBlockDocumentValue | null>(null);\n\n/** Lets the copy button tell a header placement from a floating one. */\nconst CodeBlockHeaderContext = createContext(false);\n\n/**\n * The root prepares every prop the code surface needs and publishes the\n * bundle here. `CodeBlockContent` reads it to render the surface wherever the\n * consumer composed it - typically inside their own ScrollArea - and the root\n * renders its built-in scrolling surface only when no `CodeBlockContent` is\n * found among its children.\n */\nconst CodeBlockSurfacePropsContext = createContext<CodeBlockSurfaceProps | null>(null);\n\n/**\n * `CodeBlockLineActions` renders nothing itself: it registers its render prop\n * here and the ACTIVE ROW renders it, so actions stay aligned when soft wrap\n * gives lines different heights.\n */\nexport type CodeBlockLineActionsSide = \"end\" | \"gutter\";\n\nconst CodeBlockActionsContext = createContext<{\n  register: (\n    owner: object,\n    render: CodeBlockLineActionsRender | null,\n    side?: CodeBlockLineActionsSide,\n  ) => void;\n} | null>(null);\n\n/* SSR frameworks still server-render \"use client\" files, and a bare\n   useLayoutEffect logs on every SSR pass. */\nconst useIsoLayoutEffect = typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\n\nfunction useInternalConfig(part: string): CodeBlockConfigValue {\n  const context = useContext(CodeBlockConfigContext);\n  if (!context) {\n    throw new Error(`${part} must be used within a CodeBlock`);\n  }\n  return context;\n}\n\nfunction useCodeBlockConfig(part: string): CodeBlockConfigValue & { code: string } {\n  const context = useContext(CodeBlockConfigContext);\n  const document = useContext(CodeBlockDocumentContext);\n  if (!context || !document) {\n    throw new Error(`${part} must be used within a CodeBlock`);\n  }\n  /* `code` lives on the DOCUMENT context (it changes per streamed chunk);\n     keeping it out of the config is what lets chrome that ignores the source\n     skip those re-renders. The public hook still returns it. */\n  return useMemo(() => ({ ...context, code: document.code }), [context, document.code]);\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Styling                                   */\n/* -------------------------------------------------------------------------- */\n\n/* Radius per style. Every style is named, including the zero rungs: a missing\n   rung is a style someone forgot. The parity suite enforces all-eight-or-one. */\nconst ROOT_GHOST_CLASS = \"[--code-block-bg:transparent]\";\n\nconst ROOT_RADIUS_CLASS = \"[--code-block-radius:var(--radius-lg)]\";\n\n/*\n * Colour vars use the BASE theme tokens (--card, --primary, ...), never the\n * --color-* twins: those are static @theme literals that ignore a nested\n * `.dark` scope, which once painted a white gutter cell on a forced-dark\n * block. Tints are one system, 8% light / 12% dark (yellow a touch more),\n * matching the *-light badge weights. This item ships the same cssVars.\n */\nconst ROOT_TOKEN_CLASS = [\n  \"[--code-block-padding:--spacing(3)]\",\n  \"[--code-block-gutter-width:--spacing(9)]\",\n  \"[--code-block-gutter-gap:--spacing(2.5)]\",\n  /* The fold channel sits between the numbers and the code, so both the number\n     cell's padding and the gutter width are expressed against it and a block\n     without folding pays nothing for it. */\n  \"[--code-block-fold-width:--spacing(4.5)]\",\n  \"[--code-block-number-pad:var(--code-block-gutter-gap)]\",\n  \"data-[gutter-channel]:[--code-block-number-pad:calc(var(--code-block-gutter-gap)+var(--code-block-fold-width))]\",\n  /* A diff column breathes: the +/- glyph gets a wider channel. */\n  \"data-[has-diff]:[--code-block-gutter-gap:--spacing(4)]\",\n  \"[--code-block-line-height:1.5rem]\",\n  \"[--code-block-font-size:0.8125rem]\",\n  \"[--code-block-copy-inset:--spacing(2)]\",\n  /* Where a PINNED control starts. With a header composed in, the root's top\n     edge is the header, so an unadjusted inset drops the copy button onto the\n     title row instead of the code. The header's own `min-h-9` is the height\n     assumed here, plus its 1px bottom border; override the variable for a\n     header built taller than one row. */\n  \"[--code-block-header-height:--spacing(9)]\",\n  \"[--code-block-copy-top:var(--code-block-copy-inset)]\",\n  \"data-[has-header]:[--code-block-copy-top:calc(var(--code-block-header-height)+1px+var(--code-block-copy-inset))]\",\n  /* Room the expanded block keeps under the last line for the floating\n     \"Show less\" control. */\n  \"[--code-block-expand-clearance:--spacing(9)]\",\n  /* What sits behind a sticky line number while the code scrolls under it.\n     `ghost` blanks it, because there the surface belongs to the wrapper. */\n  \"[--code-block-bg:var(--card)]\",\n  \"[--code-block-highlight-bg:color-mix(in_oklch,var(--primary)_6%,transparent)]\",\n  \"dark:[--code-block-highlight-bg:color-mix(in_oklch,var(--primary)_12%,transparent)]\",\n  \"[--code-block-highlight-bar:color-mix(in_oklch,var(--primary)_60%,transparent)]\",\n  \"[--code-block-diff-add-bg:color-mix(in_oklch,var(--success)_8%,transparent)]\",\n  \"dark:[--code-block-diff-add-bg:color-mix(in_oklch,var(--success)_12%,transparent)]\",\n  \"[--code-block-diff-remove-bg:color-mix(in_oklch,var(--destructive)_8%,transparent)]\",\n  \"dark:[--code-block-diff-remove-bg:color-mix(in_oklch,var(--destructive)_12%,transparent)]\",\n  \"[--code-block-error-bg:color-mix(in_oklch,var(--destructive)_8%,transparent)]\",\n  \"dark:[--code-block-error-bg:color-mix(in_oklch,var(--destructive)_12%,transparent)]\",\n  \"[--code-block-warning-bg:color-mix(in_oklch,var(--warning)_10%,transparent)]\",\n  \"dark:[--code-block-warning-bg:color-mix(in_oklch,var(--warning)_14%,transparent)]\",\n  \"[--code-block-info-bg:color-mix(in_oklch,var(--info)_8%,transparent)]\",\n  \"dark:[--code-block-info-bg:color-mix(in_oklch,var(--info)_12%,transparent)]\",\n  \"[--code-block-caret-color:var(--primary)]\",\n].join(\" \");\n\nconst ROOT_BASE_CLASS =\n  \"group/code-block relative flex min-w-0 flex-col rounded-(--code-block-radius) text-left\";\n\nconst ROOT_SURFACE_CLASS = \"border border-border bg-card text-card-foreground bg-clip-padding\";\n\n/**\n * The token colour switch: tokens carry only `--cb-c` / `--cb-cd`, and these\n * two rules recolour the whole block per theme instead of a `dark:` utility on\n * every span. The `color:` hint is load-bearing: `text-[var(--x)]` is\n * type-ambiguous to Tailwind and emits NO rule at all, which renders as a\n * highlighter that silently failed. Same reason the font size uses `length:`.\n */\nconst TOKEN_COLOR_CLASS =\n  \"[&_[data-slot=code-block-token]]:text-[color:var(--cb-c,currentColor)] dark:[&_[data-slot=code-block-token]]:text-[color:var(--cb-cd,currentColor)]\";\n\n/**\n * Line numbers are a CSS counter, never DOM: a pseudo-element cannot join a\n * text selection, so copying yields exact source, and the gutter costs zero\n * elements. `startLine` is a counter reset on the pre.\n */\nconst LINE_NUMBER_CLASS = [\n  \"[[data-code-line-numbers]_&]:before:pointer-events-none\",\n  \"[[data-code-line-numbers]_&]:before:sticky\",\n  \"[[data-code-line-numbers]_&]:before:left-0\",\n  \"[[data-code-line-numbers]_&]:before:z-10\",\n  \"[[data-code-line-numbers]_&]:before:-ml-(--code-block-gutter-width)\",\n  \"[[data-code-line-numbers]_&]:before:inline-block\",\n  \"[[data-code-line-numbers]_&]:before:w-(--code-block-gutter-width)\",\n  \"[[data-code-line-numbers]_&]:before:pl-(--code-block-padding)\",\n  \"[[data-code-line-numbers]_&]:before:pr-(--code-block-number-pad)\",\n  \"[[data-code-line-numbers]_&]:before:bg-(--code-block-bg)\",\n  \"[[data-code-line-numbers]_&]:before:text-right\",\n  \"[[data-code-line-numbers]_&]:before:tabular-nums\",\n  \"[[data-code-line-numbers]_&]:before:text-muted-foreground/50\",\n  \"[[data-code-line-numbers]_&]:before:select-none\",\n  \"[[data-code-line-numbers]_&]:before:[counter-increment:cb-line]\",\n  \"[[data-code-line-numbers]_&]:before:content-[counter(cb-line)]\",\n  /* A line carrying its own gutter label (a patch's dual numbers, a hunk\n     marker) renders that instead of the counter. */\n  \"[[data-code-line-numbers]_&[data-gutter]]:before:content-[attr(data-gutter)]\",\n  \"[[data-code-line-numbers]_&[data-gutter]]:before:whitespace-pre\",\n].join(\" \");\n\n/** Diff glyphs ride in the same pseudo-element family, so they never copy. */\n/**\n * The diff glyph gets its own channel, centred in the left padding (no\n * numbers) or in the number-to-code gap. A fixed `left-1` had ~1px of\n * clearance and would land on the first character.\n */\nconst LINE_DIFF_CLASS = [\n  \"data-[diff]:after:absolute\",\n  /* Above the number cell. That cell is sticky with an opaque backdrop at z-10,\n     so a glyph sharing its column is painted out at the default z. */\n  \"data-[diff]:after:z-20\",\n  \"data-[diff]:after:left-0\",\n  \"data-[diff]:after:w-(--code-block-padding)\",\n  \"data-[diff]:after:text-center\",\n  \"[[data-code-line-numbers]_&]:data-[diff]:after:-left-(--code-block-gutter-gap)\",\n  \"[[data-code-line-numbers]_&]:data-[diff]:after:w-(--code-block-gutter-gap)\",\n  \"data-[diff]:after:select-none\",\n  \"data-[diff=add]:after:text-success\",\n  \"data-[diff=remove]:after:text-destructive\",\n  \"data-[diff=add]:after:content-['+']\",\n  \"data-[diff=remove]:after:content-['-']\",\n].join(\" \");\n\nconst LINE_STATE_CLASS = [\n  \"data-[highlighted]:bg-(--code-block-highlight-bg)\",\n  \"data-[highlighted]:shadow-[inset_2px_0_0_0_var(--code-block-highlight-bar)]\",\n  \"data-[diff=add]:bg-(--code-block-diff-add-bg)\",\n  \"data-[diff=remove]:bg-(--code-block-diff-remove-bg)\",\n  \"data-[level=error]:bg-(--code-block-error-bg)\",\n  \"data-[level=warning]:bg-(--code-block-warning-bg)\",\n  \"data-[level=info]:bg-(--code-block-info-bg)\",\n  \"data-[selected]:bg-accent\",\n  /* Arrow navigation needs a visible position (WCAG 2.4.7); gated on the\n     selectable listbox so plain blocks stay inert on hover. */\n  \"[[data-selectable]_&]:data-[active]:bg-accent/60\",\n].join(\" \");\n\n/**\n * Focus mode dims the rest and clears on hover.\n *\n * Blur alone makes a snippet unreadable if the reader wanted the surrounding\n * context after all, so hovering the block restores everything.\n */\n/**\n * Motion is streaming-only and compositor-only: rows keep identity across\n * chunks, so the entry animation fires once per NEW line and a static block\n * never animates. Tokens deliberately do not animate (the plain tail gaining\n * colour remounts them mid-read). Reduced motion disables the row entry.\n */\nconst LINE_MOTION_CLASS =\n  \"[[data-streaming]_&]:animate-in [[data-streaming]_&]:fade-in-0 [[data-streaming]_&]:slide-in-from-bottom-1 [[data-streaming]_&]:duration-150 [[data-streaming]_&]:ease-out motion-reduce:animate-none\";\n\n/**\n * Deliberately no per-token animation. The highlighter runs a chunk behind\n * the stream and REPLACES the plain-text fallback lines with tokenised ones,\n * which remounts every token span; a mount-keyed fade therefore re-flashed\n * whole lines that were already readable, 300ms behind the caret. Colour\n * arriving instantly reads as highlighting; re-fading reads as a glitch.\n */\nconst TOKEN_MOTION_CLASS = \"\";\n\nconst LINE_FOCUS_CLASS =\n  \"data-[blurred]:opacity-40 data-[blurred]:blur-[1.5px] data-[blurred]:transition-[opacity,filter] group-hover/code-block:data-[blurred]:opacity-100 group-hover/code-block:data-[blurred]:blur-none\";\n\n/**\n * `data-code-line`, not `data-line`: rehype-pretty-code apps ship a global\n * `[data-line] span { color !important }` rule, and reusing its attribute\n * rendered every token monochrome. Cost a real investigation.\n */\nconst LINE_BASE_CLASS = cn(\n  \"relative block min-h-(--code-block-line-height) w-full px-(--code-block-padding) leading-(--code-block-line-height)\",\n  /* With numbers on, the left inset lives inside the sticky cell instead, so\n     paying it here too would restore the double count the gutter just lost. */\n  \"[[data-code-line-numbers]_&]:pl-0\",\n);\n\n/* -------------------------------------------------------------------------- */\n/*                                    Folding                                  */\n/* -------------------------------------------------------------------------- */\n\nexport type CodeBlockFoldRegion = {\n  /** Source line that owns the toggle and stays visible when folded. */\n  start: number;\n  /** Last source line the region swallows. */\n  end: number;\n};\n\nconst INDENT_RE = /^[ \\t]*/;\n\n/**\n * Regions come from indentation, not the grammar: shiki returns tokens, no\n * AST, and indentation is language-agnostic where a brace matcher is not.\n * A tab counts as two columns; only self-consistency matters.\n */\nfunction computeFoldRegions(lines: CodeBlockLine[]): CodeBlockFoldRegion[] {\n  const indent = (text: string) => (INDENT_RE.exec(text)?.[0] ?? \"\").replace(/\\t/g, \"  \").length;\n  const blank = (text: string) => text.trim().length === 0;\n\n  /* One pass with an open-region stack instead of a nested scan per line:\n     the old shape was quadratic on monotonically indenting files and re-ran\n     per streamed chunk. A region closes when a non-blank line returns to its\n     opener's indent; trailing blanks belong to whatever follows. */\n  const regions: CodeBlockFoldRegion[] = [];\n  const stack: { start: number; indent: number; last: number }[] = [];\n  let previous: { number: number; indent: number } | null = null;\n\n  for (const line of lines) {\n    if (blank(line.text)) continue;\n    const own = indent(line.text);\n\n    while (stack.length && own <= stack[stack.length - 1].indent) {\n      const open = stack.pop();\n      if (open && open.last > open.start) {\n        regions.push({ start: open.start, end: open.last });\n      }\n    }\n    for (const open of stack) open.last = line.number;\n\n    if (previous && own > previous.indent) {\n      stack.push({\n        start: previous.number,\n        indent: previous.indent,\n        last: line.number,\n      });\n    }\n    previous = { number: line.number, indent: own };\n  }\n\n  for (let open = stack.pop(); open; open = stack.pop()) {\n    if (open.last > open.start) regions.push({ start: open.start, end: open.last });\n  }\n\n  return regions.sort((a, b) => a.start - b.start);\n}\n\n/** Frozen so the disabled path keeps a stable identity across renders. */\nconst EMPTY_FOLD_REGIONS: CodeBlockFoldRegion[] = [];\n\n/* -------------------------------------------------------------------------- */\n/*                                    Tokens                                   */\n/* -------------------------------------------------------------------------- */\n\ntype TokenStyle = CSSProperties & Record<\"--cb-c\" | \"--cb-cd\", string>;\n\nfunction tokenStyle(token: CodeBlockToken): CSSProperties | undefined {\n  if (!token.color && !token.colorDark && !token.fontStyle) return undefined;\n\n  const style: Partial<TokenStyle> = {};\n  if (token.color) style[\"--cb-c\"] = token.color;\n  if (token.colorDark) style[\"--cb-cd\"] = token.colorDark;\n  if (token.fontStyle === \"italic\") style.fontStyle = \"italic\";\n  if (token.fontStyle === \"bold\") style.fontWeight = 700;\n  if (token.fontStyle === \"underline\") style.textDecoration = \"underline\";\n  return style as CSSProperties;\n}\n\nfunction CodeBlockTokens({ tokens }: { tokens: CodeBlockToken[] }) {\n  return (\n    <>\n      {tokens.map((token, index) => {\n        const style = tokenStyle(token);\n\n        /* An unstyled token needs no element at all. Whitespace and\n           punctuation are most of a file, so this removes most of the spans. */\n        if (!style && !token.word) {\n          // biome-ignore lint/suspicious/noArrayIndexKey: a line's tokens are positional; the row re-renders as a whole\n          return <span key={index}>{token.content}</span>;\n        }\n\n        return (\n          <span\n            // biome-ignore lint/suspicious/noArrayIndexKey: a line's tokens are positional; the row re-renders as a whole\n            key={index}\n            data-slot=\"code-block-token\"\n            data-word={token.word || undefined}\n            className={cn(\n              token.word &&\n                \"bg-primary/10 ring-primary/20 rounded-sm px-0.5 py-px ring-1 ring-inset\",\n            )}\n            style={style}\n          >\n            {token.content}\n          </span>\n        );\n      })}\n    </>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                     Line                                    */\n/* -------------------------------------------------------------------------- */\n\ntype CodeBlockLineRowProps = {\n  line: CodeBlockLine;\n  selectable: boolean;\n  selected: boolean;\n  focusMode: boolean;\n  active: boolean;\n  caret: boolean;\n  actions?: CodeBlockLineActionsRender | null;\n  actionsSide?: CodeBlockLineActionsSide;\n  domIdBase?: string;\n  startLine?: number;\n  onSelect?: (line: number, extend: boolean) => void;\n  foldable?: boolean;\n  foldRegion?: CodeBlockFoldRegion;\n  folded?: boolean;\n  onToggleFold?: (start: number) => void;\n};\n\n/**\n * Plain reference-equality `memo`, on purpose: the highlighter returns the\n * SAME object for an unchanged line, so a streamed chunk re-renders one row.\n * A custom comparator would spend that win walking every token.\n */\nconst CodeBlockLineRow = memo(function CodeBlockLineRow({\n  line,\n  selectable,\n  selected,\n  focusMode,\n  active,\n  caret,\n  actions,\n  actionsSide = \"end\",\n  domIdBase,\n  startLine = 1,\n  onSelect,\n  foldRegion,\n  folded,\n  onToggleFold,\n}: CodeBlockLineRowProps) {\n  const state = line.state;\n  const blurred = focusMode && !state?.focused;\n  const lineDomId = domIdBase ? `${domIdBase}-L${line.number}` : undefined;\n  const hiddenCount = foldRegion ? foldRegion.end - foldRegion.start : 0;\n  /* The fold toggle owns the channel, so a fold-start row keeps its chevron\n     and sends the action back to the row end. */\n  const inGutter = actionsSide === \"gutter\" && !foldRegion;\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: an option when selectable; the listbox handles the keyboard (aria-activedescendant)\n    // biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-selected comes with role=\"option\", both only when selectable\n    <span\n      data-slot=\"code-block-line\"\n      data-code-line={line.number}\n      data-gutter={line.gutter}\n      id={lineDomId}\n      data-active={active || undefined}\n      data-highlighted={state?.highlighted || undefined}\n      data-diff={state?.diff}\n      data-focused={state?.focused || undefined}\n      data-blurred={blurred || undefined}\n      data-level={state?.level}\n      data-selected={selected || undefined}\n      role={selectable ? \"option\" : undefined}\n      aria-selected={selectable ? selected : undefined}\n      tabIndex={selectable ? -1 : undefined}\n      onMouseDown={\n        /* Shift-click is line-range selection here, but the browser also reads\n           it as \"extend the text selection\", which paints a stray highlight\n           across the block. Suppressing it only for the shift case leaves plain\n           drag-to-select, and therefore copying, untouched. */\n        selectable\n          ? (event) => {\n              if (event.shiftKey) event.preventDefault();\n            }\n          : undefined\n      }\n      onClick={\n        selectable && onSelect ? (event) => onSelect(line.number, event.shiftKey) : undefined\n      }\n      className={cn(\n        LINE_BASE_CLASS,\n        LINE_NUMBER_CLASS,\n        LINE_STATE_CLASS,\n        LINE_DIFF_CLASS,\n        LINE_FOCUS_CLASS,\n        LINE_MOTION_CLASS,\n        selectable && \"cursor-pointer\",\n      )}\n      /**\n       * Every row re-seeds the counter, making its number absolute rather\n       * than ordinal. That serves two features at once: folding can remove\n       * rows without renumbering the tail, and `content-visibility` can skip\n       * offscreen rows - a style-contained row cannot read a shared counter,\n       * which once rendered every gutter number as 1.\n       */\n      style={{ counterReset: `cb-line ${line.number - 1}` }}\n    >\n      {foldRegion && onToggleFold ? (\n        <button\n          type=\"button\"\n          /* Inside a role=\"option\" row a focusable descendant is invalid ARIA,\n             so the toggle leaves the tab order when the block is selectable;\n             pointer use is unaffected. */\n          tabIndex={selectable ? -1 : undefined}\n          data-slot=\"code-block-fold-toggle\"\n          data-state={folded ? \"folded\" : \"unfolded\"}\n          aria-expanded={!folded}\n          aria-label={\n            folded\n              ? `Unfold lines ${foldRegion.start + startLine - 1} to ${foldRegion.end + startLine - 1}`\n              : `Fold lines ${foldRegion.start + startLine - 1} to ${foldRegion.end + startLine - 1}`\n          }\n          onClick={(event) => {\n            /* The row itself may be a selection target, and a fold is not a\n               selection. */\n            event.stopPropagation();\n            onToggleFold(foldRegion.start);\n          }}\n          className={cn(\n            /* Styled as a ghost xs button rather than composed from Button:\n               the ghost variant paints aria-expanded backgrounds, which would\n               keep every UNFOLDED toggle lit, and its icon rungs do not fit\n               an 18px channel. Hover feedback only; folded state reads\n               through color. */\n            /* Square, centred in the line box: height matches the width and\n               the translate makes up the difference to the 24px line. Open\n               regions read darker than folded ones. */\n            \"text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 sticky z-20 inline-flex size-(--code-block-fold-width) shrink-0 translate-y-[calc((var(--code-block-line-height)-var(--code-block-fold-width))/2)] cursor-pointer items-center justify-center rounded-[4px] align-top outline-none focus-visible:ring-2\",\n            /* Margins cancel: -(4px + width) then +4px, so the toggle paints\n               in the channel, sits 4px from the code, and the code still\n               starts where a row without a toggle does. An uncancelled\n               margin slid every foldable row and clipped characters. The\n               spacing() shorthand is written as a rem literal because\n               `mr-(--spacing(1))` is not valid Tailwind and generates no\n               rule at all - measured live as a 4px drift. */\n            \"-ml-[calc(var(--code-block-fold-width)+0.25rem)]\",\n            \"mr-[0.25rem]\",\n            \"left-[calc(var(--code-block-gutter-width)-var(--code-block-fold-width)-0.25rem)]\",\n            folded && \"text-muted-foreground/60\",\n          )}\n        >\n          {folded ? (\n            <ChevronRightIcon aria-hidden=\"true\" strokeWidth={2.5} className=\"size-3\" />\n          ) : (\n            <ChevronDownIcon aria-hidden=\"true\" strokeWidth={2.5} className=\"size-3\" />\n          )}\n        </button>\n      ) : null}\n      {actions && active ? (\n        // biome-ignore lint/a11y/noStaticElementInteractions: only stops the click reaching a selectable row\n        <span\n          data-slot=\"code-block-line-actions\"\n          data-side={inGutter ? \"gutter\" : \"end\"}\n          contentEditable={false}\n          /* An action is a control, not line content. Without this the click\n             bubbles to a selectable row and toggles the very selection the\n             action just consumed, so \"add these 6 lines\" left line 6 selected\n             behind it. The consumer's own handler has already run by here. */\n          onClick={(event) => event.stopPropagation()}\n          className={cn(\n            /* Inside the <pre>, so the code surface's mono face, pre\n               whitespace and token color would cascade into real buttons\n               placed here. Reset to app typography at the boundary. */\n            \"text-foreground z-20 items-center gap-1 font-sans whitespace-normal select-none\",\n            !inGutter && \"absolute end-2 top-0 flex h-(--code-block-line-height)\",\n            /* ABSOLUTE and centred on the row's START EDGE: `left-0` is where\n               the code column begins (the row box already excludes the\n               gutter), and the -50% translate hangs the control half over the\n               number column, half over the code - the editor treatment. Being\n               out of flow on BOTH axes is the point: an in-flow version\n               pushed the code right by its own width, and an earlier block\n               level one broke the row onto a new line. The control travels\n               with the code on a horizontal scroll; it cannot also be sticky. */\n            inGutter &&\n              \"absolute top-1/2 left-0 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center\",\n          )}\n        >\n          {actions({ line: line.number, text: line.text, state })}\n        </span>\n      ) : null}\n      <span data-slot=\"code-block-line-content\">\n        {/* An empty line needs an explicit break. Copying a selection relies on\n            the browser emitting a newline between block-level rows, and a row\n            with no text at all contributes neither text nor separator, so a\n            blank line silently vanishes from the pasted source. */}\n        {line.tokens.length === 0 ? <br /> : null}\n        <CodeBlockTokens tokens={line.tokens} />\n        {folded && hiddenCount > 0 ? (\n          /* `select-none` is doing real work: a manual drag-select over a\n             folded block would otherwise paste this chip into the middle of\n             the source. The copy button reads the raw code and never sees it. */\n          <button\n            type=\"button\"\n            tabIndex={selectable ? -1 : undefined}\n            data-slot=\"code-block-fold-marker\"\n            aria-label={`Unfold ${hiddenCount} hidden lines`}\n            onClick={(event) => {\n              event.stopPropagation();\n              onToggleFold?.(line.number);\n            }}\n            /* Same centring as the fold toggle: chip height matches the fold\n               channel and the translate makes up the difference to the line\n               box, so the label sits on the code baseline's visual centre\n               instead of riding high. */\n            className=\"bg-muted/80 text-muted-foreground hover:bg-accent hover:text-accent-foreground ml-2 inline-flex h-(--code-block-fold-width) translate-y-[calc((var(--code-block-line-height)-var(--code-block-fold-width))/2)] cursor-pointer items-center rounded-sm px-1.5 align-top text-[0.8em] leading-none select-none\"\n          >\n            {`... ${hiddenCount} lines`}\n          </button>\n        ) : null}\n        {caret ? (\n          <span\n            data-slot=\"code-block-caret\"\n            aria-hidden=\"true\"\n            /* An underline caret: a thin strip on the baseline, the shape a\n               terminal uses, instead of a bold block that outweighs the text\n               it follows. */\n            className=\"ml-0.5 inline-block h-[0.12em] w-[0.55em] translate-y-[0.02em] animate-pulse rounded-[1px] bg-(--code-block-caret-color) motion-reduce:animate-none\"\n          />\n        ) : null}\n      </span>\n    </span>\n  );\n});\n\n/* -------------------------------------------------------------------------- */\n/*                                     Root                                    */\n/* -------------------------------------------------------------------------- */\n\nexport type CodeBlockProps = {\n  code?: string;\n  language?: string;\n  lines?: CodeBlockLine[];\n  themes?: CodeBlockThemes;\n  highlight?: boolean;\n  showLineNumbers?: boolean;\n  startLine?: number;\n  wrap?: boolean;\n  defaultWrap?: boolean;\n  onWrapChange?: (wrap: boolean) => void;\n  maxLines?: number;\n  variant?: \"default\" | \"ghost\";\n  label?: string;\n  highlightedLines?: CodeBlockLineSpec;\n  highlightedWords?: CodeBlockWordSpec[];\n  focusedLines?: CodeBlockLineSpec;\n  diff?: CodeBlockDiffSpec;\n  lineLevels?: CodeBlockLevelSpec;\n  transformers?: CodeBlockTransformer[];\n  streaming?: boolean;\n  selectable?: boolean;\n  selectedLines?: number[];\n  defaultSelectedLines?: number[];\n  onSelectedLinesChange?: (lines: number[]) => void;\n  /** Screen-reader text announced when a stream finishes, for localisation. */\n  completeAnnouncement?: string;\n  /** Collapsed state under `maxLines`, controlled. */\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  /** Detects fold regions from indentation and renders a toggle per region. */\n  foldable?: boolean;\n  /**\n   * Replaces the indentation heuristic with your own regions (source-numbered,\n   * like every line spec) - grammar folding, JSON blocks, patch hunks.\n   * Implies nothing about `foldable`; pass both.\n   */\n  foldRegions?: CodeBlockFoldRegion[];\n  /** Folded regions by start line, 1-based within `code`. Controlled. */\n  folded?: number[];\n  /** Folded regions by their start line, uncontrolled. */\n  defaultFolded?: number[];\n  onFoldedChange?: (folded: number[]) => void;\n  children?: ReactNode;\n} & Omit<ComponentProps<\"div\">, \"children\" | \"onSelect\">;\n\n/**\n * The root. `<CodeBlock code={code} language=\"tsx\" />` is a complete block;\n * passing `lines` from a server component skips the client highlighter and\n * shiki entirely. Children are CHROME ONLY: the root renders the code surface\n * itself, so child order never matters and a wrapper cannot break scrolling.\n */\nfunction CodeBlock({\n  code,\n  language,\n  lines: linesProp,\n  themes,\n  highlight = true,\n  showLineNumbers = false,\n  startLine = 1,\n  wrap: wrapProp,\n  defaultWrap = false,\n  onWrapChange,\n  maxLines,\n  variant = \"default\",\n  label,\n  highlightedLines,\n  highlightedWords,\n  focusedLines,\n  diff,\n  lineLevels,\n  transformers,\n  streaming = false,\n  selectable = false,\n  selectedLines,\n  defaultSelectedLines,\n  onSelectedLinesChange,\n  completeAnnouncement,\n  expanded: expandedProp,\n  defaultExpanded = false,\n  onExpandedChange,\n  foldable = false,\n  foldRegions: foldRegionsProp,\n  folded,\n  defaultFolded,\n  onFoldedChange,\n  className,\n  children,\n  ...props\n}: CodeBlockProps) {\n  const contentId = useId();\n  const source = code ?? \"\";\n  const resolvedLanguage = resolveCodeBlockLanguage(language);\n\n  /* The deferred copy is what a fast stream tokenises against. React keeps the\n     previous render on screen while the new one is prepared, so the block never\n     blanks and the main thread is never blocked by a chunk. */\n  const deferredSource = useDeferredValue(source);\n\n  const plainLines = useMemo(\n    () => toPlainLines(deferredSource, startLine),\n    [deferredSource, startLine],\n  );\n\n  /* The result is tagged with the exact inputs that produced it. Without the\n     tag, swapping `code` kept rendering the PREVIOUS document for the whole\n     async highlight pass - or spliced two files together when the old result\n     was shorter than the new source. */\n  const [highlighted, setHighlighted] = useState<{\n    source: string;\n    spec: string;\n    lines: CodeBlockLine[];\n  } | null>(null);\n\n  const shouldHighlight = !linesProp && highlight && Boolean(resolvedLanguage) && source.length > 0;\n\n  /* Presentation props enter the effect as ONE serialized key: they are\n     inline literals at most call sites, and reference deps would re-tokenize\n     per parent render. The effect parses its inputs back out of the key, so\n     used and depended-on values cannot drift. `transformers` holds functions\n     and stays a reference dep - hoist it to module scope. */\n  const specKey = JSON.stringify([\n    themes ?? null,\n    startLine,\n    highlightedLines ?? null,\n    highlightedWords ?? null,\n    focusedLines ?? null,\n    diff ?? null,\n    lineLevels ?? null,\n  ]);\n\n  const transformersWarnedRef = useRef(false);\n  const previousTransformersRef = useRef(transformers);\n  useEffect(() => {\n    if (\n      process.env.NODE_ENV !== \"production\" &&\n      streaming &&\n      previousTransformersRef.current !== transformers &&\n      !transformersWarnedRef.current\n    ) {\n      transformersWarnedRef.current = true;\n      console.warn(\n        \"[code-block] `transformers` changed identity during a stream, which \" +\n          \"re-tokenizes the whole document per chunk. Hoist the array to \" +\n          \"module scope or memoize it.\",\n      );\n    }\n    previousTransformersRef.current = transformers;\n  }, [transformers, streaming]);\n\n  useEffect(() => {\n    if (!shouldHighlight) {\n      setHighlighted(null);\n      return;\n    }\n\n    const [\n      specThemes,\n      specStartLine,\n      specHighlightedLines,\n      specHighlightedWords,\n      specFocusedLines,\n      specDiff,\n      specLineLevels,\n    ] = JSON.parse(specKey) as [\n      CodeBlockThemes | null,\n      number,\n      CodeBlockLineSpec | null,\n      CodeBlockWordSpec[] | null,\n      CodeBlockLineSpec | null,\n      CodeBlockDiffSpec | null,\n      CodeBlockLevelSpec | null,\n    ];\n\n    let active = true;\n    void highlightCode(deferredSource, {\n      language,\n      instanceKey: contentId,\n      themes: specThemes ?? undefined,\n      transformers,\n      startLine: specStartLine,\n      highlightedLines: specHighlightedLines ?? undefined,\n      highlightedWords: specHighlightedWords ?? undefined,\n      focusedLines: specFocusedLines ?? undefined,\n      diff: specDiff ?? undefined,\n      lineLevels: specLineLevels ?? undefined,\n    }).then((next) => {\n      if (active) {\n        setHighlighted({ source: deferredSource, spec: specKey, lines: next });\n      }\n    });\n\n    return () => {\n      active = false;\n    };\n  }, [shouldHighlight, deferredSource, language, transformers, specKey, contentId]);\n\n  /* Plain text is the floor, never a blank frame: the highlighter runs a\n     chunk behind a stream, so its lines show as-is and the remainder appends\n     as plain text that gains colour a frame later. */\n  const lines = useMemo(() => {\n    if (linesProp) return linesProp;\n    if (!shouldHighlight || !highlighted) return plainLines;\n    /* The merge below is only valid under the STREAMING invariant: the\n       highlighted source must be a prefix of what is on screen, with the same\n       presentation spec. A swapped `code` or spec falls back to plain text\n       for one highlight pass instead of showing the previous document. */\n    if (highlighted.spec !== specKey || !deferredSource.startsWith(highlighted.source)) {\n      return plainLines;\n    }\n    if (highlighted.lines.length >= plainLines.length) return highlighted.lines;\n    return [...highlighted.lines, ...plainLines.slice(highlighted.lines.length)];\n  }, [linesProp, shouldHighlight, highlighted, plainLines, deferredSource, specKey]);\n\n  const focusMode = useMemo(() => lines.some((line) => line.state?.focused), [lines]);\n\n  const hasDiff = useMemo(() => lines.some((line) => line.state?.diff), [lines]);\n\n  const [internalWrap, setInternalWrap] = useState(defaultWrap);\n  const wrap = wrapProp ?? internalWrap;\n  const setWrap = useCallback(\n    (next: boolean) => {\n      if (wrapProp === undefined) setInternalWrap(next);\n      onWrapChange?.(next);\n    },\n    [wrapProp, onWrapChange],\n  );\n\n  const collapsible = typeof maxLines === \"number\" && lines.length > maxLines;\n  const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);\n  const expanded = expandedProp ?? internalExpanded;\n  const setExpanded = useCallback(\n    (next: boolean) => {\n      if (expandedProp === undefined) setInternalExpanded(next);\n      onExpandedChange?.(next);\n    },\n    [expandedProp, onExpandedChange],\n  );\n  const isExpanded = collapsible ? expanded : true;\n\n  const [internalSelected, setInternalSelected] = useState<number[]>(defaultSelectedLines ?? []);\n  const selection = selectedLines ?? internalSelected;\n  const selectedSet = useMemo(() => new Set(selection), [selection]);\n  const lastSelectedRef = useRef<number | null>(null);\n\n  /* Latest-value refs, so the toggle callbacks below can be created ONCE.\n     With state in their dep lists every selection or fold change minted new\n     identities, which reached every row through `onSelect`/`onToggleFold` and\n     busted the row memo the streaming design rests on. */\n  const selectionStateRef = useRef({\n    selection,\n    selectedSet,\n    controlled: selectedLines !== undefined,\n    onSelectedLinesChange,\n  });\n  selectionStateRef.current = {\n    selection,\n    selectedSet,\n    controlled: selectedLines !== undefined,\n    onSelectedLinesChange,\n  };\n\n  const commitSelection = useCallback((next: number[]) => {\n    if (!selectionStateRef.current.controlled) setInternalSelected(next);\n    selectionStateRef.current.onSelectedLinesChange?.(next);\n  }, []);\n\n  const toggleLine = useCallback(\n    (line: number, extend: boolean) => {\n      const anchor = lastSelectedRef.current;\n      const { selection, selectedSet } = selectionStateRef.current;\n\n      if (extend && anchor !== null) {\n        const from = Math.min(anchor, line);\n        const to = Math.max(anchor, line);\n        const range: number[] = [];\n        for (let value = from; value <= to; value += 1) range.push(value);\n        commitSelection(range);\n        return;\n      }\n\n      lastSelectedRef.current = line;\n      commitSelection(\n        selectedSet.has(line)\n          ? selection.filter((value) => value !== line)\n          : [...selection, line].sort((a, b) => a - b),\n      );\n    },\n    [commitSelection],\n  );\n\n  /* Regions are SOURCE-numbered (1-based within `code`), like every other\n     line spec, so `folded` values and `highlightedLines` values interchange. */\n  const foldRegions = useMemo(() => {\n    if (!foldable) return EMPTY_FOLD_REGIONS;\n    if (foldRegionsProp) return foldRegionsProp;\n    return computeFoldRegions(lines).map((region) => ({\n      start: region.start - startLine + 1,\n      end: region.end - startLine + 1,\n    }));\n  }, [foldable, foldRegionsProp, lines, startLine]);\n\n  const foldStarts = useMemo(\n    () => new Map(foldRegions.map((region) => [region.start, region])),\n    [foldRegions],\n  );\n\n  const [internalFolded, setInternalFolded] = useState<number[]>(defaultFolded ?? []);\n  const foldedValue = folded ?? internalFolded;\n  const foldedSet = useMemo(() => new Set(foldedValue), [foldedValue]);\n\n  /* Same latest-value shape as selection, for the same row-memo reason. */\n  const foldStateRef = useRef({\n    foldedValue,\n    foldedSet,\n    controlled: folded !== undefined,\n    onFoldedChange,\n  });\n  foldStateRef.current = {\n    foldedValue,\n    foldedSet,\n    controlled: folded !== undefined,\n    onFoldedChange,\n  };\n\n  const commitFolded = useCallback((next: number[]) => {\n    if (!foldStateRef.current.controlled) setInternalFolded(next);\n    foldStateRef.current.onFoldedChange?.(next);\n  }, []);\n\n  const toggleFold = useCallback(\n    (start: number) => {\n      const { foldedValue, foldedSet } = foldStateRef.current;\n      commitFolded(\n        foldedSet.has(start)\n          ? foldedValue.filter((value) => value !== start)\n          : [...foldedValue, start].sort((a, b) => a - b),\n      );\n    },\n    [commitFolded],\n  );\n\n  /**\n   * A line is hidden when ANY folded region covers it, and that one rule is\n   * what makes nesting work without a tree: folding an outer region swallows\n   * the inner toggles as well, their own folded state survives untouched\n   * underneath, and unfolding the outer one restores exactly what the reader\n   * left behind rather than a flattened block.\n   */\n  const renderedLines = useMemo(() => {\n    if (!foldable || foldedSet.size === 0) return lines;\n    const hidden = new Set<number>();\n    for (const start of foldedSet) {\n      const region = foldStarts.get(start);\n      if (!region) continue;\n      for (let n = region.start + 1; n <= region.end; n += 1) {\n        hidden.add(n + startLine - 1);\n      }\n    }\n    return lines.filter((line) => !hidden.has(line.number));\n  }, [foldable, foldedSet, foldStarts, lines, startLine]);\n\n  const configValue = useMemo<CodeBlockConfigValue>(\n    () => ({\n      language,\n      resolvedLanguage,\n      showLineNumbers,\n      wrap,\n      setWrap,\n      wrapControlled: wrapProp !== undefined,\n      expanded: isExpanded,\n      setExpanded,\n      collapsible,\n      streaming,\n      contentId,\n    }),\n    [\n      language,\n      resolvedLanguage,\n      showLineNumbers,\n      wrap,\n      setWrap,\n      wrapProp,\n      isExpanded,\n      setExpanded,\n      collapsible,\n      streaming,\n      contentId,\n    ],\n  );\n\n  const clearSelection = useCallback(() => commitSelection([]), [commitSelection]);\n\n  const documentValue = useMemo<CodeBlockDocumentValue>(\n    () => ({\n      code: source,\n      lines,\n      selected: selectedSet,\n      selectable,\n      toggleLine,\n      clearSelection,\n      foldable,\n      foldRegions,\n      folded: foldedSet,\n      toggleFold,\n      setFolded: commitFolded,\n    }),\n    [\n      source,\n      lines,\n      selectedSet,\n      selectable,\n      toggleLine,\n      clearSelection,\n      foldable,\n      foldRegions,\n      foldedSet,\n      toggleFold,\n      commitFolded,\n    ],\n  );\n\n  const visibleLines = collapsible && !isExpanded ? maxLines : undefined;\n\n  /* Detected from the element type rather than a `:has()` selector. This repo\n     has measured `:has()` at 85-120ms of style invalidation per DOM mutation,\n     and a streaming block mutates on every chunk, so the primitive uses none. */\n  const hasHeader = containsElementType(children, CodeBlockHeader);\n\n  const [lineActions, setLineActions] = useState<CodeBlockLineActionsRender | null>(null);\n  const [actionsSide, setActionsSide] = useState<CodeBlockLineActionsSide>(\"end\");\n  const actionsOwnerRef = useRef<object | null>(null);\n  const actionsRegistry = useMemo(\n    () => ({\n      /* Wrapped in a setter callback: a render prop IS a function, so passing\n         it to setState bare would run it as a state updater. Ownership makes\n         a stale unmount a no-op and makes a SECOND live group loud in dev -\n         the registry holds one render prop, so the second silently won. */\n      register: (\n        owner: object,\n        render: CodeBlockLineActionsRender | null,\n        side: CodeBlockLineActionsSide = \"end\",\n      ) => {\n        if (render) {\n          if (\n            process.env.NODE_ENV !== \"production\" &&\n            actionsOwnerRef.current &&\n            actionsOwnerRef.current !== owner\n          ) {\n            console.warn(\n              \"[code-block] Two CodeBlockLineActions are mounted; the block \" +\n                \"renders ONE action group and the later mount replaces the \" +\n                \"earlier one.\",\n            );\n          }\n          actionsOwnerRef.current = owner;\n        } else if (actionsOwnerRef.current !== owner) {\n          return;\n        } else {\n          actionsOwnerRef.current = null;\n        }\n        setLineActions(() => render);\n        setActionsSide(side);\n      },\n    }),\n    [],\n  );\n\n  /* One channel between the numbers and the code, shared by the fold toggle\n     and a gutter-side action. Reserved only when something asks for it, so a\n     plain block keeps the gutter it always had. */\n  /* Only FOLDING reserves gutter space. A gutter-side action floats over the\n     row's start edge instead of widening anything, so a block with actions is\n     geometrically identical to one without. */\n  const gutterChannel = foldable;\n\n  /* Walked rather than registered, so server HTML never carries a doubled\n     surface. Sees through plain wrappers; a consumer component boundary is\n     opaque, which the dev-mode guard in CodeBlockContent reports. */\n  const hasContent = containsElementType(children, CodeBlockContent);\n\n  const surfaceProps: CodeBlockSurfaceProps = {\n    lines: renderedLines,\n    gutterMax: lines[lines.length - 1]?.number ?? startLine,\n    foldable,\n    gutterChannel,\n    actionsSide,\n    foldStarts,\n    foldedSet,\n    onToggleFold: toggleFold,\n    expandClearance: collapsible && isExpanded,\n    wrap,\n    startLine,\n    showLineNumbers,\n    focusMode,\n    selectable,\n    selectedSet,\n    toggleLine,\n    visibleLines,\n    streaming,\n    label: label ?? (language ? `${language} code` : \"Code\"),\n    contentId,\n    actions: lineActions,\n    completeAnnouncement,\n    builtInSurfaceRendered: !hasContent,\n  };\n\n  return (\n    <CodeBlockConfigContext.Provider value={configValue}>\n      <CodeBlockDocumentContext.Provider value={documentValue}>\n        <CodeBlockActionsContext.Provider value={actionsRegistry}>\n          <div\n            data-slot=\"code-block\"\n            data-variant={variant}\n            data-streaming={streaming || undefined}\n            data-has-diff={hasDiff || undefined}\n            data-foldable={foldable || undefined}\n            data-gutter-channel={gutterChannel || undefined}\n            data-has-header={hasHeader || undefined}\n            className={cn(\n              ROOT_BASE_CLASS,\n              ROOT_RADIUS_CLASS,\n              ROOT_TOKEN_CLASS,\n              variant === \"default\" && ROOT_SURFACE_CLASS,\n              variant === \"ghost\" && ROOT_GHOST_CLASS,\n              className,\n            )}\n            {...props}\n          >\n            <CodeBlockSurfacePropsContext.Provider value={surfaceProps}>\n              {children}\n              {!hasContent && <CodeBlockSurface {...surfaceProps} />}\n            </CodeBlockSurfacePropsContext.Provider>\n          </div>\n        </CodeBlockActionsContext.Provider>\n      </CodeBlockDocumentContext.Provider>\n    </CodeBlockConfigContext.Provider>\n  );\n}\n\n/**\n * The code surface as a composable part: inside your own ScrollArea the block\n * stops scrolling internally and the ancestor owns both axes (`maxLines`'\n * height cap no longer applies). Omitted, the root renders the same surface\n * itself, exactly as before.\n */\nfunction CodeBlockContent({ className }: { className?: string }) {\n  const surface = useContext(CodeBlockSurfacePropsContext);\n  if (!surface) {\n    throw new Error(\"CodeBlockContent must be used within a CodeBlock\");\n  }\n\n  /* The render-time walk cannot see through a consumer's own component\n     boundary; when that happens the root also renders its built-in surface\n     and the code appears twice. Silent double content is the worst failure\n     mode, so development says exactly what to do. */\n  useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return;\n    if (surface.builtInSurfaceRendered) {\n      console.error(\n        \"CodeBlockContent is hidden behind a component boundary, so CodeBlock \" +\n          \"also rendered its built-in surface and the code appears twice. \" +\n          \"Compose <CodeBlockContent /> directly in the CodeBlock's children \" +\n          \"(plain wrappers like a ScrollArea are fine).\",\n      );\n    }\n  }, [surface.builtInSurfaceRendered]);\n\n  return <CodeBlockSurface {...surface} scroll={false} className={className} />;\n}\n\n/**\n * Sees through plain element wrappers (a ScrollArea, a div) but not through a\n * consumer's own component boundary - unrendered children are opaque. Compose\n * `CodeBlockContent` directly in the block's subtree.\n */\nfunction containsElementType(node: ReactNode, type: unknown): boolean {\n  for (const child of Children.toArray(node)) {\n    if (!isValidElement(child)) continue;\n    if (child.type === type) return true;\n    const inner = (child.props as { children?: ReactNode }).children;\n    if (inner && containsElementType(inner, type)) return true;\n  }\n  return false;\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Surface                                   */\n/* -------------------------------------------------------------------------- */\n\ntype CodeBlockSurfaceProps = {\n  lines: CodeBlockLine[];\n  wrap: boolean;\n  startLine: number;\n  showLineNumbers: boolean;\n  focusMode: boolean;\n  selectable: boolean;\n  selectedSet: Set<number>;\n  toggleLine: (line: number, extend: boolean) => void;\n  visibleLines?: number;\n  gutterMax: number;\n  foldable: boolean;\n  gutterChannel: boolean;\n  actionsSide: CodeBlockLineActionsSide;\n  foldStarts: Map<number, CodeBlockFoldRegion>;\n  foldedSet: Set<number>;\n  onToggleFold: (start: number) => void;\n  expandClearance: boolean;\n  streaming: boolean;\n  label: string;\n  contentId: string;\n  actions?: CodeBlockLineActionsRender | null;\n  completeAnnouncement?: string;\n  /** true when the root rendered its own surface (no CodeBlockContent found). */\n  builtInSurfaceRendered?: boolean;\n  /** false = no scroll container of its own; an ancestor scrolls instead. */\n  scroll?: boolean;\n  className?: string;\n};\n\nfunction CodeBlockSurface({\n  lines,\n  wrap,\n  startLine,\n  showLineNumbers,\n  focusMode,\n  selectable,\n  selectedSet,\n  toggleLine,\n  visibleLines,\n  gutterMax,\n  foldable,\n  gutterChannel,\n  actionsSide,\n  foldStarts,\n  foldedSet,\n  onToggleFold,\n  expandClearance,\n  streaming,\n  label,\n  contentId,\n  actions,\n  completeAnnouncement,\n  scroll = true,\n  className,\n}: CodeBlockSurfaceProps) {\n  const viewportRef = useRef<HTMLDivElement | null>(null);\n  const preRef = useRef<HTMLPreElement | null>(null);\n  const [activeLine, setActiveLine] = useState<number | null>(null);\n  const stickRef = useRef(true);\n\n  /* Stick-to-bottom disengages the moment the reader scrolls up, and re-engages\n     when they come back. Reading the scroll position on the event is enough\n     here because nothing is written back during the same frame. */\n  /* Announced once when a stream ends, so a screen reader learns the code is\n     complete without hearing it assembled character by character. */\n  const [announcement, setAnnouncement] = useState(\"\");\n  const wasStreaming = useRef(streaming);\n  useEffect(() => {\n    if (wasStreaming.current && !streaming) {\n      setAnnouncement(completeAnnouncement ?? `Code generation complete, ${lines.length} lines.`);\n    }\n    wasStreaming.current = streaming;\n  }, [streaming, lines.length, completeAnnouncement]);\n\n  /* Stick-to-bottom follows whichever element ACTUALLY scrolls - writing\n     scrollTop to the composed mode's inert viewport was a silent no-op.\n     Resolved per commit: a short block becomes its own scroller the moment\n     it outgrows its cap. */\n  const resolveScroller = useCallback((): HTMLElement | null => {\n    const viewport = viewportRef.current;\n    if (!viewport) return null;\n    if (viewport.scrollHeight > viewport.clientHeight + 1) return viewport;\n\n    let node: HTMLElement | null = viewport.parentElement;\n    while (node && node !== document.body) {\n      const overflowY = getComputedStyle(node).overflowY;\n      if (\n        (overflowY === \"auto\" || overflowY === \"scroll\" || overflowY === \"overlay\") &&\n        node.scrollHeight > node.clientHeight + 1\n      ) {\n        return node;\n      }\n      node = node.parentElement;\n    }\n\n    /* An AI transcript's default shape scrolls the PAGE. The walk above never\n       reaches the root elements, so without this branch the stream wrote\n       scrollTop to an inert viewport and drifted off screen. */\n    const page = document.scrollingElement as HTMLElement | null;\n    if (page && page.scrollHeight > page.clientHeight + 1) return page;\n\n    return viewport;\n  }, []);\n\n  const handleScroll = useCallback(() => {\n    const viewport = viewportRef.current;\n    if (!viewport) return;\n    const distance = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;\n    stickRef.current = distance < 24;\n  }, []);\n\n  /* The ancestor case needs its own listener: the reader's intent has to be\n     recorded BEFORE the next chunk grows the content, or the distance already\n     includes what just arrived. Re-attached per chunk rather than diffed,\n     which is a handful of listener swaps a second and keeps this correct when\n     the scrolling element changes underneath. */\n  // biome-ignore lint/correctness/useExhaustiveDependencies: re-attached per chunk (lines.length), as the scroller can change\n  useEffect(() => {\n    if (!streaming) return;\n    const scroller = resolveScroller();\n    if (!scroller || scroller === viewportRef.current) return;\n\n    const onScroll = () => {\n      const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;\n      stickRef.current = distance < 24;\n    };\n\n    /* Page scrolls fire on window, not on the scrolling element. */\n    const target: EventTarget = scroller === document.scrollingElement ? window : scroller;\n    target.addEventListener(\"scroll\", onScroll, { passive: true });\n    return () => target.removeEventListener(\"scroll\", onScroll);\n  }, [streaming, lines.length, resolveScroller]);\n\n  useIsoLayoutEffect(() => {\n    if (!streaming || !stickRef.current) return;\n    const scroller = resolveScroller();\n    if (scroller) scroller.scrollTop = scroller.scrollHeight;\n  }, [lines, streaming, resolveScroller]);\n\n  /* One listener for the whole surface rather than a handler per line: the\n     active row is resolved from the event target, so the floating action group\n     costs one element no matter how long the file is. Focus is tracked as well\n     as hover, or the actions would be unreachable without a pointer. */\n  const trackActive = useCallback(\n    (event: { target: EventTarget | null }) => {\n      if (!actions && !selectable) return;\n      const target = event.target as HTMLElement | null;\n      const row = target?.closest?.(\"[data-code-line]\") as HTMLElement | null;\n      const value = row?.getAttribute(\"data-code-line\");\n\n      /* No row under the pointer means the gutter, which lives in the <pre>'s\n       PADDING and so is covered by no row element. Keep the active line\n       rather than clearing it: clearing here unmounted a gutter action the\n       moment the pointer left the code toward it, so the control vanished\n       before it could be pressed. `onPointerLeave` still clears on exit. */\n      if (!value) return;\n      setActiveLine(Number(value));\n    },\n    [actions, selectable],\n  );\n\n  const clearActive = useCallback(() => setActiveLine(null), []);\n\n  /* Rows and keyboard speak DISPLAYED numbers (the gutter, data-code-line);\n     selection state speaks SOURCE numbers like every other line spec. This is\n     the one conversion point between them. */\n  const toggleDisplayed = useCallback(\n    (displayed: number, extend: boolean) => toggleLine(displayed - startLine + 1, extend),\n    [toggleLine, startLine],\n  );\n\n  const handleKeyDown = useCallback(\n    (event: ReactKeyboardEvent<HTMLElement>) => {\n      if (!selectable) return;\n\n      if (event.key === \"Escape\") {\n        setActiveLine(null);\n        return;\n      }\n\n      /* Toggle IN PLACE. Shift+Arrow selects while moving, but without this a\n         keyboard user could never deselect a single line. */\n      if ((event.key === \"Enter\" || event.key === \" \") && activeLine !== null) {\n        event.preventDefault();\n        toggleDisplayed(activeLine, event.shiftKey);\n        return;\n      }\n\n      const isArrow = event.key === \"ArrowDown\" || event.key === \"ArrowUp\";\n      if (!isArrow && event.key !== \"Home\" && event.key !== \"End\") return;\n      event.preventDefault();\n\n      /* Step by RENDERED index, not by number arithmetic: folding removes\n         rows, and numeric stepping walked the active line into hidden lines\n         where it silently vanished. */\n      let next: number;\n      if (event.key === \"Home\") {\n        next = lines[0]?.number ?? startLine;\n      } else if (event.key === \"End\") {\n        next = lines[lines.length - 1]?.number ?? startLine;\n      } else {\n        const step = event.key === \"ArrowDown\" ? 1 : -1;\n        const index = lines.findIndex((line) => line.number === activeLine);\n        const nextIndex =\n          index === -1\n            ? step === 1\n              ? 0\n              : lines.length - 1\n            : Math.min(lines.length - 1, Math.max(0, index + step));\n        next = lines[nextIndex]?.number ?? startLine;\n      }\n\n      setActiveLine(next);\n      const row = preRef.current?.querySelector<HTMLElement>(`[data-code-line=\"${next}\"]`);\n      row?.scrollIntoView({ block: \"nearest\" });\n      if (isArrow && event.shiftKey) toggleDisplayed(next, true);\n    },\n    [selectable, lines, startLine, activeLine, toggleDisplayed],\n  );\n\n  const lastLineNumber = lines[lines.length - 1]?.number;\n\n  /* `startLine` is just a counter reset. Gutter width follows the widest\n     number in the DOCUMENT (not the folded view, or folding the tail would\n     shift every row); `ch` is one digit in the mono `pre`, and the floor of 2\n     stops the column twitching as a stream crosses line 9. Lines carrying\n     their own gutter label (a patch's dual numbers) widen it to the longest\n     label instead. */\n  const gutterDigits = Math.max(\n    2,\n    String(gutterMax ?? startLine).length,\n    ...lines.map((line) => line.gutter?.length ?? 0),\n  );\n\n  const surfaceStyle = {\n    \"--code-block-gutter-width\": gutterChannel\n      ? `calc(${gutterDigits}ch + var(--code-block-fold-width) + var(--code-block-gutter-gap) + var(--code-block-padding))`\n      : `calc(${gutterDigits}ch + var(--code-block-gutter-gap) + var(--code-block-padding))`,\n    counterReset: `cb-line ${startLine - 1}`,\n  } as CSSProperties;\n\n  return (\n    <div\n      data-slot=\"code-block-content\"\n      className={cn(\n        \"relative min-w-0 rounded-[inherit]\",\n        /* Clipping is the scroll-mode contract only: composed, a clipped pre\n           would hide its own width from the ancestor's viewport and the\n           horizontal scrollbar would never render. */\n        scroll && \"flex-1 overflow-hidden\",\n      )}\n    >\n      {/* A polite status, deliberately NOT an aria-live region on the code\n          itself: a live region over streaming code narrates every token. */}\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </span>\n      {/* A plain overflow container, deliberately not a ScrollArea primitive.\n          The scroll must stay INSIDE the block (the sticky gutter, the\n          stick-to-bottom stream and `maxLines` all anchor to it), but nothing\n          here needs custom scrollbar machinery, and dropping it leaves the\n          block with no UI-library dependency at all - which is why both twins\n          are the same file. Compose Card, Frame, or your own ScrollArea\n          around the block for outer framing. */}\n      {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-activedescendant points into the listbox it hosts when selectable */}\n      <div\n        ref={viewportRef}\n        data-slot=\"code-block-viewport\"\n        id={contentId}\n        role=\"region\"\n        aria-label={label}\n        aria-busy={streaming || undefined}\n        aria-activedescendant={\n          selectable && activeLine !== null ? `${contentId}-L${activeLine}` : undefined\n        }\n        // biome-ignore lint/a11y/noNoninteractiveTabindex: a scrollable region must be focusable to scroll without a pointer\n        tabIndex={0}\n        onScroll={handleScroll}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          \"focus-visible:ring-ring/50 relative min-w-0 rounded-[inherit] outline-none focus-visible:ring-[3px]\",\n          /* Composed under an ancestor scroller the surface scrolls NOTHING\n             itself: both axes belong to the ancestor, whose viewport picks up\n             the pre's overflowing width, and the sticky number cell anchors\n             to that viewport instead. Mixed ownership shipped once and read\n             as two scrollbar styles on one block. */\n          scroll && \"[scrollbar-width:thin] overflow-auto\",\n          scroll && visibleLines && \"max-h-(--cb-max-height)\",\n          className,\n        )}\n        style={\n          scroll && visibleLines\n            ? ({\n                \"--cb-max-height\": `calc(${visibleLines} * var(--code-block-line-height) + 2 * var(--code-block-padding))`,\n              } as CSSProperties)\n            : undefined\n        }\n      >\n        {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: role=\"listbox\" and aria-multiselectable are set together when selectable */}\n        <pre\n          ref={preRef}\n          dir=\"ltr\"\n          data-code-line-numbers={showLineNumbers || undefined}\n          data-wrap={wrap || undefined}\n          role={selectable ? \"listbox\" : undefined}\n          data-selectable={selectable || undefined}\n          aria-multiselectable={selectable || undefined}\n          aria-label={selectable ? `${label} lines` : undefined}\n          onPointerOver={trackActive}\n          onPointerLeave={clearActive}\n          onFocus={trackActive}\n          className={cn(\n            \"w-max min-w-full py-(--code-block-padding) font-mono text-(length:--code-block-font-size)\",\n            TOKEN_COLOR_CLASS,\n            TOKEN_MOTION_CLASS,\n            /* `min-w-0` must beat the `min-w-full` above, or the pre keeps\n                   a floor of 100% of a scroll container that is itself wider\n                   than the viewport, and wrapping still leaves a stray\n                   horizontal scrollbar. */\n            wrap && \"w-full max-w-full min-w-0 break-words whitespace-pre-wrap\",\n            showLineNumbers && \"pl-(--code-block-gutter-width)\",\n            /* The expand control floats over the surface in BOTH states, so\n                   an expanded block reserves a row of clearance rather than\n                   growing a bordered footer under the code. */\n            expandClearance && \"pb-(--code-block-expand-clearance)\",\n          )}\n          style={surfaceStyle}\n        >\n          {/* `content-visibility: auto` is a measured dead end on BOTH axes:\n                  its style containment breaks the shared gutter counter\n                  (every number renders as 1), and per-row it still fails -\n                  the paint containment clips the number cell, which is\n                  deliberately pulled left OUT of the row's box (verified in\n                  Chrome: forcing one row visible restored only that row's\n                  number). Windowing here means rendering fewer rows, not\n                  containing them. */}\n          {/* presentation, so the listbox owns its options directly. */}\n          <code data-slot=\"code-block-code\" role={selectable ? \"presentation\" : undefined}>\n            {lines.map((line) => (\n              <CodeBlockLineRow\n                key={line.number}\n                line={line}\n                selectable={selectable}\n                selected={selectedSet.has(line.number - startLine + 1)}\n                focusMode={focusMode}\n                active={activeLine === line.number}\n                caret={streaming && line.number === lastLineNumber}\n                actions={activeLine === line.number ? actions : null}\n                actionsSide={actionsSide}\n                domIdBase={contentId}\n                startLine={startLine}\n                onSelect={toggleDisplayed}\n                foldable={foldable}\n                foldRegion={foldStarts.get(line.number - startLine + 1)}\n                folded={foldedSet.has(line.number - startLine + 1)}\n                onToggleFold={onToggleFold}\n              />\n            ))}\n          </code>\n        </pre>\n      </div>\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Chrome                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Opt-in header. Absent, the block renders no chrome, so it nests in a Card or\n * Frame without a doubled bar. Presence flips `data-has-header` on the root,\n * detected from the element type rather than a `:has()` selector.\n */\nfunction CodeBlockHeader({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <CodeBlockHeaderContext.Provider value={true}>\n      <div\n        data-slot=\"code-block-header\"\n        className={cn(\n          \"border-border flex min-h-9 shrink-0 items-center gap-2 border-b px-3 py-0 text-sm\",\n          className,\n        )}\n        {...props}\n      />\n    </CodeBlockHeaderContext.Provider>\n  );\n}\n\nfunction CodeBlockTitle({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"code-block-title\"\n      className={cn(\"text-muted-foreground min-w-0 truncate font-mono text-xs\", className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * The resolved language as a label, no props needed. It shows what the\n * highlighter actually resolved, so an unsupported grammar becomes visible\n * instead of silently rendering plain.\n */\nfunction CodeBlockLanguage({ className, children, ...props }: ComponentProps<\"span\">) {\n  const { language, resolvedLanguage } = useInternalConfig(\"CodeBlockLanguage\");\n  const label = children ?? resolvedLanguage ?? language;\n\n  if (!label) return null;\n\n  return (\n    <span\n      data-slot=\"code-block-language\"\n      data-unsupported={!resolvedLanguage || undefined}\n      className={cn(\n        \"border-border bg-muted/60 text-muted-foreground inline-flex shrink-0 items-center rounded-md border px-1.5 py-0.5 font-mono text-[0.6875rem] leading-none uppercase\",\n        className,\n      )}\n      {...props}\n    >\n      {label}\n    </span>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Copy button                                */\n/* -------------------------------------------------------------------------- */\n\nexport type CodeBlockCopyButtonProps = {\n  value?: string;\n  timeout?: number;\n  onCopy?: (value: string) => void;\n  onCopyError?: (error: unknown) => void;\n  /** Accessible name and announced outcomes, for localisation. */\n  labels?: { copy?: string; copied?: string; failed?: string };\n  /** `pinned` floats over the surface; `inline` sits in a header. */\n  position?: \"auto\" | \"pinned\" | \"inline\";\n  alwaysVisible?: boolean;\n} & Omit<ComponentProps<typeof Button>, \"value\" | \"onCopy\" | \"children\"> & {\n    children?: ReactNode;\n  };\n\n/**\n * Copy button. Inline inside a header, pinned over the surface anywhere else.\n * Pinning is absolute against the root, not `position: sticky`: sticky still\n * slides away under HORIZONTAL scroll, and code scrolls sideways constantly.\n * Notation comments are stripped from the payload, matching what is rendered.\n */\nfunction CodeBlockCopyButton({\n  value,\n  timeout = 2000,\n  onCopy,\n  onCopyError,\n  labels,\n  position = \"auto\",\n  alwaysVisible = false,\n  className,\n  variant = \"ghost\",\n  size,\n  children,\n  ...props\n}: CodeBlockCopyButtonProps) {\n  const inHeader = useContext(CodeBlockHeaderContext);\n\n  const resolvedPosition = position === \"auto\" ? (inHeader ? \"inline\" : \"pinned\") : position;\n\n  /* A header is chrome, so the button takes the smaller icon rung there and the\n     full one when it floats over the code. Both are per-style ladders in the\n     shadcn button, so this stays style-aware instead of pinning a pixel size. */\n  const resolvedSize = size ?? (resolvedPosition === \"inline\" ? \"icon-sm\" : \"icon\");\n\n  const document_ = useContext(CodeBlockDocumentContext);\n\n  /* Server-highlighted blocks pass `lines` and no `code`, so the document\n     text is the fallback; without it this rendered a labelled control that\n     did nothing on the exact path the docs advertise. */\n  const payload = useCallback(() => {\n    const fallback = document_?.code || document_?.lines.map((line) => line.text).join(\"\\n\");\n    return stripNotationComments(value ?? fallback ?? \"\");\n  }, [value, document_]);\n\n  return (\n    <CopyButton\n      data-slot=\"code-block-copy\"\n      data-position={resolvedPosition}\n      value={payload}\n      timeout={timeout}\n      onCopy={onCopy}\n      onCopyError={onCopyError}\n      labels={{\n        copy: labels?.copy ?? \"Copy code\",\n        ...(labels?.copied && { copied: labels.copied }),\n        ...(labels?.failed && { failed: labels.failed }),\n      }}\n      variant={variant}\n      size={resolvedSize}\n      className={cn(\n        \"shrink-0 [&_svg]:size-3.5\",\n        resolvedPosition === \"pinned\" &&\n          /* Physical `right`, not logical `end`: the code surface is forced\n             dir=\"ltr\", so in an RTL app the logical inset landed the button\n             over the first characters instead of the trailing edge. */\n          \"absolute top-(--code-block-copy-top) right-(--code-block-copy-inset) z-20\",\n        resolvedPosition === \"pinned\" &&\n          !alwaysVisible &&\n          \"opacity-0 transition-opacity group-hover/code-block:opacity-100 focus-visible:opacity-100\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </CopyButton>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Wrap and expand                                */\n/* -------------------------------------------------------------------------- */\n\ntype CodeBlockButtonProps = Omit<ComponentProps<typeof Button>, \"children\"> & {\n  children?: ReactNode;\n};\n\nfunction CodeBlockWrapToggle({\n  className,\n  children,\n  variant = \"ghost\",\n  size = \"sm\",\n  onPress,\n  ...props\n}: CodeBlockButtonProps) {\n  const { wrap, setWrap } = useInternalConfig(\"CodeBlockWrapToggle\");\n\n  return (\n    <Button\n      data-slot=\"code-block-wrap-toggle\"\n      data-state={wrap ? \"on\" : \"off\"}\n      aria-pressed={wrap}\n      variant={variant}\n      size={size}\n      onPress={(event) => {\n        onPress?.(event);\n        setWrap(!wrap);\n      }}\n      className={cn(\"shrink-0\", className)}\n      {...props}\n    >\n      {children ?? (wrap ? \"No wrap\" : \"Wrap\")}\n    </Button>\n  );\n}\n\n/**\n * Expands a block collapsed by `maxLines`.\n *\n * Renders nothing when the content is shorter than the cut, so a consumer can\n * always compose it without guarding on line count themselves.\n */\nfunction CodeBlockExpandButton({\n  className,\n  children,\n  variant = \"ghost\",\n  size = \"sm\",\n  onPress,\n  ...props\n}: CodeBlockButtonProps) {\n  const { expanded, setExpanded, collapsible, contentId } =\n    useInternalConfig(\"CodeBlockExpandButton\");\n\n  if (!collapsible) return null;\n\n  return (\n    <div\n      data-slot=\"code-block-expand\"\n      data-state={expanded ? \"expanded\" : \"collapsed\"}\n      className={cn(\n        /* Both states float over the surface. Expanded used to rejoin the flex\n           column as a bordered strip, which read as a card footer and moved the\n           block's bottom edge on the first click; the surface reserves a row of\n           clearance instead, so only the label changes. */\n        \"absolute inset-x-0 bottom-0 z-10 order-last flex items-end justify-center pt-10 pb-2\",\n        /* The fade means \"there is more below\", so it belongs to the collapsed\n           state only. Expanded, the surface already reserves a clear row for\n           this control, and keeping the gradient just dimmed the last real\n           line of code. */\n        !expanded && \"from-card via-card/85 bg-gradient-to-t to-transparent\",\n        className,\n      )}\n    >\n      <Button\n        aria-expanded={expanded}\n        aria-controls={contentId}\n        variant={variant}\n        size={size}\n        onPress={(event) => {\n          onPress?.(event);\n          setExpanded(!expanded);\n        }}\n        {...props}\n      >\n        {children ?? (expanded ? \"Show less\" : \"Show more\")}\n      </Button>\n    </div>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                 Line actions                                */\n/* -------------------------------------------------------------------------- */\n\nexport type CodeBlockLineActionsProps = {\n  children: CodeBlockLineActionsRender;\n  /**\n   * `end` floats the group over the end of the active row. `gutter` puts it in\n   * the channel beside the line number, which is where a \"add this line\"\n   * affordance belongs. A fold toggle owns that channel when both are on, so a\n   * gutter action on a fold-start row falls back to `end`.\n   */\n  side?: CodeBlockLineActionsSide;\n};\n\n/** File extensions for the download button's default name. */\nconst LANGUAGE_EXTENSIONS: Record<string, string> = {\n  bash: \"sh\",\n  c: \"c\",\n  cpp: \"cpp\",\n  csharp: \"cs\",\n  css: \"css\",\n  diff: \"patch\",\n  docker: \"dockerfile\",\n  go: \"go\",\n  graphql: \"graphql\",\n  html: \"html\",\n  java: \"java\",\n  javascript: \"js\",\n  json: \"json\",\n  jsx: \"jsx\",\n  kotlin: \"kt\",\n  markdown: \"md\",\n  php: \"php\",\n  python: \"py\",\n  ruby: \"rb\",\n  rust: \"rs\",\n  sql: \"sql\",\n  swift: \"swift\",\n  toml: \"toml\",\n  tsx: \"tsx\",\n  typescript: \"ts\",\n  xml: \"xml\",\n  yaml: \"yaml\",\n};\n\nexport type CodeBlockDownloadButtonProps = {\n  /** Text to save. Defaults to the block's code with notation stripped. */\n  value?: string;\n  /** Defaults to `code.<ext>` from the block's language. */\n  filename?: string;\n  onDownload?: (filename: string) => void;\n  /** `pinned` floats over the surface; `inline` sits in a header. */\n  position?: \"auto\" | \"pinned\" | \"inline\";\n  alwaysVisible?: boolean;\n  /** Accessible name, for localisation. */\n  label?: string;\n} & Omit<ComponentProps<typeof Button>, \"value\" | \"children\"> & {\n    children?: ReactNode;\n  };\n\n/**\n * Saves the block's code as a file - the sibling the copy button was missing\n * for builder-style output, where the result IS a file and disk is the next\n * step. Same placement contract as the copy button.\n */\nfunction CodeBlockDownloadButton({\n  value,\n  filename,\n  onDownload,\n  position = \"auto\",\n  alwaysVisible = false,\n  label,\n  className,\n  variant = \"ghost\",\n  size,\n  children,\n  ...props\n}: CodeBlockDownloadButtonProps) {\n  const context = useContext(CodeBlockConfigContext);\n  const document_ = useContext(CodeBlockDocumentContext);\n  const inHeader = useContext(CodeBlockHeaderContext);\n\n  const resolvedPosition = position === \"auto\" ? (inHeader ? \"inline\" : \"pinned\") : position;\n  const resolvedSize = size ?? (resolvedPosition === \"inline\" ? \"icon-sm\" : \"icon\");\n\n  const handleClick = useCallback(() => {\n    const fallback = document_?.code || document_?.lines.map((line) => line.text).join(\"\\n\");\n    const payload = stripNotationComments(value ?? fallback ?? \"\");\n    if (!payload || typeof window === \"undefined\") return;\n\n    const extension = LANGUAGE_EXTENSIONS[context?.resolvedLanguage ?? \"\"] ?? \"txt\";\n    const name = filename ?? `code.${extension}`;\n    const url = URL.createObjectURL(new Blob([payload], { type: \"text/plain;charset=utf-8\" }));\n    const anchor = window.document.createElement(\"a\");\n    anchor.href = url;\n    anchor.download = name;\n    anchor.click();\n    /* Deferred: revoking synchronously races the browser starting the save. */\n    window.setTimeout(() => URL.revokeObjectURL(url), 1000);\n    onDownload?.(name);\n  }, [value, filename, context, document_, onDownload]);\n\n  return (\n    <Button\n      data-slot=\"code-block-download\"\n      data-position={resolvedPosition}\n      aria-label={label ?? \"Download code\"}\n      variant={variant}\n      size={resolvedSize}\n      onPress={handleClick}\n      className={cn(\n        \"shrink-0 [&_svg]:size-3.5\",\n        resolvedPosition === \"pinned\" &&\n          \"absolute top-(--code-block-copy-top) right-(--code-block-copy-inset) z-20\",\n        resolvedPosition === \"pinned\" &&\n          !alwaysVisible &&\n          \"opacity-0 transition-opacity group-hover/code-block:opacity-100 focus-visible:opacity-100\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? <DownloadIcon aria-hidden=\"true\" />}\n    </Button>\n  );\n}\n\n/**\n * Actions on the hovered or focused line. ONE group exists for the whole\n * block, rendered inside the active row: a 2,000 line file mounts one button\n * group, not 2,000.\n */\nfunction CodeBlockLineActions({ children, side = \"end\" }: CodeBlockLineActionsProps) {\n  const registry = useContext(CodeBlockActionsContext);\n  const owner = useRef({}).current;\n\n  useEffect(() => {\n    registry?.register(owner, children, side);\n    return () => registry?.register(owner, null);\n  }, [registry, owner, children, side]);\n\n  return null;\n}\n\n/**\n * Live folding for anything composed inside the block - a \"fold all\" control,\n * a region count. Mirrors `useCodeBlockSelection`; region and start numbers\n * are source-based like every line spec.\n */\nfunction useCodeBlockFolding() {\n  const document = useContext(CodeBlockDocumentContext);\n  if (!document) {\n    throw new Error(\"useCodeBlockFolding must be used within a CodeBlock\");\n  }\n\n  const { foldable, foldRegions, folded, toggleFold, setFolded } = document;\n  const foldedStarts = useMemo(() => [...folded].sort((a, b) => a - b), [folded]);\n  const foldAll = useCallback(\n    () => setFolded(foldRegions.map((region) => region.start)),\n    [setFolded, foldRegions],\n  );\n  const unfoldAll = useCallback(() => setFolded([]), [setFolded]);\n\n  return {\n    foldable,\n    regions: foldRegions,\n    foldedStarts,\n    toggleFold,\n    foldAll,\n    unfoldAll,\n  };\n}\n\n/**\n * Live selection for anything composed inside the block - a line action, a\n * header control. Returns the selected lines as a sorted array plus the same\n * toggle the rows use, so an \"add selection to chat\" affordance can act on the\n * whole range instead of only the hovered line.\n */\nfunction useCodeBlockSelection() {\n  const document = useContext(CodeBlockDocumentContext);\n  if (!document) {\n    throw new Error(\"useCodeBlockSelection must be used within a CodeBlock\");\n  }\n\n  const { selected, selectable, toggleLine, clearSelection } = document;\n  const selectedLines = useMemo(() => [...selected].sort((a, b) => a - b), [selected]);\n\n  return { selectable, selectedLines, toggleLine, clearSelection };\n}\n\nexport type {\n  CodeBlockDiffSpec,\n  CodeBlockHighlightOptions,\n  CodeBlockLevelSpec,\n  CodeBlockLine,\n  CodeBlockLineActionsRender,\n  CodeBlockLineSpec,\n  CodeBlockLineState,\n  CodeBlockPatchFile,\n  CodeBlockThemes,\n  CodeBlockToken,\n  CodeBlockTransformer,\n  CodeBlockWordSpec,\n} from \"@/lib/code-block-highlight\";\nexport {\n  ansiToLines,\n  parseUnifiedDiff,\n} from \"@/lib/code-block-highlight\";\n/* Re-exported so a consumer wires an AI transcript from one import path. */\nexport {\n  CodeBlock,\n  CodeBlockContent,\n  CodeBlockCopyButton,\n  CodeBlockDownloadButton,\n  CodeBlockExpandButton,\n  CodeBlockHeader,\n  CodeBlockLanguage,\n  CodeBlockLineActions,\n  CodeBlockTitle,\n  CodeBlockWrapToggle,\n  markdownCodeProps,\n  markdownFences,\n  useCodeBlockConfig,\n  useCodeBlockFolding,\n  useCodeBlockSelection,\n};\n",
      "type": "registry:component",
      "target": "components/fma-ds/code-block.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "third-party",
    "license": "MIT",
    "origin": "reui.io (https://reui.io/docs/components/base/code-block, MIT); buttons rebuilt on react-aria Button",
    "tags": [
      "data-display",
      "code",
      "syntax-highlighting",
      "shiki",
      "ai"
    ],
    "contentHash": "38255a4d8d09"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}