{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "type": "registry:ui",
  "title": "Code block",
  "description": "Code in an answer, a tool's output or a diff: mono on a quiet surface with a 32px header that holds the file name and a Copy button. tsx, ts, jsx and js are highlighted in the foreground's tones only (strings take a hint of the accent text), a diff shows a + in success and a - in danger on a faint band with hidden \"Added\" and \"Removed\" words, line numbers sit in a gutter that is not selected or copied, and long lines scroll sideways inside a focusable, labelled block. Static.",
  "dependencies": [
    "sugar-high"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/button.json",
    "https://littmd.vercel.app/r/icons.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/code-block.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/code-block 2026.11.06. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * CodeBlock: code in an answer, in a tool's output, or a diff (components.md, \"Code block\").\n *\n * - Surface: `--litt-surface-2` with an inset `--litt-line` hairline at the surface radius, drawn as an overlay so a\n *   changed line's band never paints over it. A 32px header holds the file name or the language in mono at\n *   the caption size in `--litt-fg-subtle`, and the Copy button. The header is its own row, so the button can never\n *   cover code. The code is mono at the small size in `--litt-fg`, padded `--litt-space-4`, with a tab size of 2.\n * - Flat: a grey card (--litt-surface-2, as Press, so a diff row's tint keeps its words at 4.5:1) with no hairline, and the Copy key a round white key on it.\n * - Highlighting: tsx, ts, jsx and js go through sugar-high, and every other language is plain. Code is not\n *   data, so the colors are the foreground's tones and the accent text for strings (no data hues, R7).\n *   sugar-high paints each token with a syntax variable, so this file declares all of them on the block\n *   itself with arbitrary properties. It needs no stylesheet from the gallery and ships alone.\n * - Diff: with lang \"diff\" each line's first character is its sign, read into its own column: `+` in\n *   `--litt-success`, `-` in `--litt-danger`. Added lines are `--litt-fg`, removed lines `--litt-fg-subtle`, and both changed kinds\n *   take an fg 4% band that runs the whole scroll width (each row owns the side padding, so the band reaches\n *   the edges). Status color stays on the sign. Diff lines are not syntax-highlighted, so a removed line can\n *   step back to `--litt-fg-subtle`. Header lines before the first hunk (`diff`, `---`, `+++`) and `@@` lines show\n *   as quiet text with no sign.\n * - Screen readers: the sign column is hidden from them and a changed line starts with hidden words, \"Added:\"\n *   or \"Removed:\". Color and glyphs are never the only signal. The hidden words cannot be selected, so a hand\n *   selection never picks them up.\n * - Line numbers: an optional gutter in mono at the caption size in `--litt-fg-subtle`. It is hidden from screen\n *   readers, cannot be selected, and is not part of Copy.\n * - Overflow: lines scroll sideways inside the block (`wrap` wraps them), and a block taller than `maxHeight`\n *   scrolls down. The scroller is focusable and named by `label`, so a keyboard can scroll it.\n * - Copy: the button copies `code` exactly as given, a diff with its signs included. It swaps its icon for a\n *   check at once, and a polite live region says \"Copied\". No toast.\n * - Touch: the header and the button grow to 44px on a coarse pointer.\n * - Focus: the scroller and the Copy button both draw the 2px focus ring inside themselves, because the button\n *   fills the header's height and an outer ring would run past the block's edge.\n * - Motion: none. The copy icon swaps at once.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from \"react\";\nimport { highlight } from \"sugar-high\";\nimport { Button } from \"./button\";\nimport { CheckIcon, CopyIcon } from \"./icons\";\n\nexport type CodeBlockProps = {\n  /** The source to show. Copy puts exactly this text on the clipboard. */\n  code: string;\n  /** Names the scroll area for screen readers and keyboard users. Defaults to the file name, or \"Code\". */\n  label?: string;\n  /** tsx, ts, jsx and js are highlighted. diff reads each line's first character as its sign. shell and text are plain. */\n  lang?: \"tsx\" | \"ts\" | \"jsx\" | \"js\" | \"diff\" | \"shell\" | \"text\";\n  /** Shown in the header in place of the language, such as \"next.config.ts\". */\n  filename?: string;\n  /** Shows a gutter of line numbers. It is hidden from screen readers, cannot be selected and is not copied. */\n  lineNumbers?: boolean;\n  /** Wraps long lines instead of scrolling them sideways. */\n  wrap?: boolean;\n  /** Caps the height of the code, which then scrolls down. A number is px, a string is any CSS length such as \"16rem\". */\n  maxHeight?: number | string;\n  /** Extra classes for the block, such as a top margin. It fills the width of its container. */\n  className?: string;\n};\n\ntype Lang = NonNullable<CodeBlockProps[\"lang\"]>;\n\n/* sugar-high paints every token with a color read from a variable of its own. Declaring those variables here, on\n   the block, maps them to the tokens in both themes and keeps the component free of any stylesheet. Strings are\n   the one place the accent text appears, mixed 90% into the foreground, so they sit quietly beside the code. */\nconst SYNTAX =\n  \"[--sh-identifier:var(--litt-fg)] [--sh-class:var(--litt-fg)] [--sh-property:var(--litt-fg)] [--sh-entity:var(--litt-fg)] \" +\n  \"[--sh-keyword:var(--litt-fg)] [--sh-sign:var(--litt-fg-muted)] [--sh-comment:var(--litt-fg-subtle)] \" +\n  \"[--sh-string:color-mix(in_oklab,var(--litt-accent-text)_90%,var(--litt-fg))] [--sh-jsxliterals:var(--sh-string)] \" +\n  \"[--sh-space:currentColor] [--sh-break:currentColor]\";\n\n/* The band behind an added or removed line: the foreground at 4%, so it follows both themes. */\nconst BAND = \"bg-[color-mix(in_oklab,var(--litt-fg)_4%,transparent)]\";\n\nconst HIGHLIGHT: Partial<Record<Lang, \"typescript\" | \"javascript\">> = {\n  tsx: \"typescript\",\n  ts: \"typescript\",\n  jsx: \"javascript\",\n  js: \"javascript\",\n};\n\ntype Kind = \"code\" | \"add\" | \"remove\" | \"context\" | \"meta\";\ntype Row = { kind: Kind; text: string; html?: string };\n\n/** One row per line of a diff. The first character is the sign: + added, - removed, a space context. */\nfunction diffRows(lines: string[]): Row[] {\n  /* With hunks, everything before the first @@ is the file header (diff, index, ---, +++), which is not a change. */\n  const hunks = lines.some((l) => l.startsWith(\"@@\"));\n  let inHunk = !hunks;\n  return lines.map((line): Row => {\n    if (line.startsWith(\"@@\")) {\n      inHunk = true;\n      return { kind: \"meta\", text: line };\n    }\n    if (!inHunk || line.startsWith(\"\\\\\")) return { kind: \"meta\", text: line };\n    if (line.startsWith(\"+\")) return { kind: \"add\", text: line.slice(1) };\n    if (line.startsWith(\"-\")) return { kind: \"remove\", text: line.slice(1) };\n    return { kind: \"context\", text: line.startsWith(\" \") ? line.slice(1) : line };\n  });\n}\n\nconst SIGN: Partial<Record<Kind, string>> = { add: \"+\", remove: \"-\" };\nconst SIGN_COLOR: Partial<Record<Kind, string>> = { add: \"text-litt-success\", remove: \"text-litt-danger\" };\n/* What a screen reader hears before a changed line. */\nconst SAID: Partial<Record<Kind, string>> = { add: \"Added: \", remove: \"Removed: \" };\nconst ROW_COLOR: Record<Kind, string> = {\n  code: \"text-litt-fg\",\n  context: \"text-litt-fg\",\n  add: `text-litt-fg ${BAND}`,\n  remove: `text-litt-fg-subtle ${BAND}`,\n  meta: \"text-litt-fg-subtle\",\n};\n\n/** The older route for pages without the async clipboard API (plain http). It must run inside the click. */\nfunction copyBySelection(text: string): boolean {\n  const area = document.createElement(\"textarea\");\n  area.value = text;\n  area.setAttribute(\"readonly\", \"\");\n  area.style.position = \"fixed\";\n  area.style.opacity = \"0\";\n  document.body.appendChild(area);\n  area.select();\n  try {\n    return document.execCommand(\"copy\");\n  } catch {\n    return false;\n  } finally {\n    area.remove();\n  }\n}\n\ntype CopyState = \"idle\" | \"copied\" | \"failed\";\n/* How long \"Copied\" stays up. It is feedback, not animation. */\nconst FEEDBACK_MS = 2000;\n\n/** Copy with feedback that needs no toast. `copy(text)` is called straight from the click, so Safari keeps the gesture. */\nfunction useCopy(): [CopyState, (text: string) => void] {\n  const [state, setState] = useState<CopyState>(\"idle\");\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => () => clearTimeout(timer.current), []);\n  const copy = useCallback((text: string) => {\n    const settle = (next: CopyState) => {\n      setState(next);\n      clearTimeout(timer.current);\n      timer.current = setTimeout(() => setState(\"idle\"), FEEDBACK_MS);\n    };\n    if (navigator.clipboard?.writeText) {\n      navigator.clipboard.writeText(text).then(\n        () => settle(\"copied\"),\n        () => settle(\"failed\"),\n      );\n    } else settle(copyBySelection(text) ? \"copied\" : \"failed\");\n  }, []);\n  return [state, copy];\n}\n\n/**\n * A block of code on a quiet surface, with a header that names the file or the language and a Copy button.\n * tsx, ts, jsx and js are highlighted in the foreground's tones; lang \"diff\" shows signs, bands and hidden\n * \"Added\" and \"Removed\" words; the rest is plain. Long lines scroll sideways inside the block, and a\n * focusable, labelled scroller lets a keyboard reach them.\n */\nexport function CodeBlock({\n  code,\n  label,\n  lang = \"text\",\n  filename,\n  lineNumbers = false,\n  wrap = false,\n  maxHeight,\n  className = \"\",\n}: CodeBlockProps) {\n  const [state, copy] = useCopy();\n  const diff = lang === \"diff\";\n\n  const rows = useMemo<Row[]>(() => {\n    /* One trailing line break is the end of the last line, not a blank line. */\n    const lines = code.replace(/\\r\\n?/g, \"\\n\").replace(/\\n$/, \"\").split(\"\\n\");\n    if (diff) return diffRows(lines);\n    const grammar = HIGHLIGHT[lang];\n    /* sugar-high escapes the source itself, so its HTML is safe to inject. It makes one <span> per line, joined by line breaks. */\n    const html = grammar ? highlight(lines.join(\"\\n\"), { lang: grammar }).split(\"\\n\") : null;\n    return lines.map((text, i) => ({ kind: \"code\", text, html: html?.[i] }));\n  }, [code, lang, diff]);\n\n  const digits = String(rows.length).length;\n  const name = label ?? filename ?? \"Code\";\n\n  return (\n    <div className={`relative min-w-0 max-w-full rounded-litt-surface bg-litt-surface-2 ${SYNTAX} ${className}`}>\n      <div className=\"flex h-litt-control-sm items-center justify-between gap-2 pl-4 pr-1 pointer-coarse:h-litt-control-md\">\n        <span className=\"min-w-0 truncate font-litt-mono text-litt-caption font-normal text-litt-fg-subtle\">{filename ?? lang}</span>\n        <Button\n          iconOnly\n          variant=\"ghost\"\n          size=\"sm\"\n          aria-label=\"Copy code\"\n          className=\"focus-visible:outline-offset-[-2px] pointer-coarse:h-litt-control-md pointer-coarse:w-litt-control-md in-[[data-material=flat]]:rounded-litt-pill in-[[data-material=flat]]:bg-litt-surface-1 in-[[data-material=flat]]:hover:bg-litt-surface-1\"\n          onClick={() => copy(code)}\n        >\n          {state === \"copied\" ? <CheckIcon /> : <CopyIcon />}\n        </Button>\n      </div>\n      {/* The scroller. A diff's bands and the gutter live in the rows, so they scroll with the code. */}\n      <pre\n        tabIndex={0}\n        role=\"group\"\n        aria-label={name}\n        style={maxHeight === undefined ? undefined : { maxHeight }}\n        className={\n          \"m-0 overflow-auto rounded-b-litt-surface py-4 font-litt-mono text-litt-small font-normal text-litt-fg [tab-size:2] \" +\n          \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-litt-focus\"\n        }\n      >\n        <code\n          style={{ \"--code-gutter\": `${digits}ch` } as CSSProperties}\n          className={wrap ? \"block w-full\" : \"block w-max min-w-full\"}\n        >\n          {rows.map((row, i) => (\n            <span key={i} className={`flex items-baseline gap-3 px-4 ${ROW_COLOR[row.kind]}`}>\n              {lineNumbers && (\n                <span aria-hidden=\"true\" className=\"w-(--code-gutter) shrink-0 select-none text-right font-litt-mono text-litt-caption font-normal text-litt-fg-subtle\">\n                  {i + 1}\n                </span>\n              )}\n              {diff && (\n                <span aria-hidden=\"true\" className={`w-[1ch] shrink-0 select-none ${SIGN_COLOR[row.kind] ?? \"\"}`}>\n                  {SIGN[row.kind]}\n                </span>\n              )}\n              <span\n                className={\"min-w-0 grow \" + (wrap ? \"whitespace-pre-wrap [overflow-wrap:anywhere]\" : \"whitespace-pre\")}\n              >\n                {SAID[row.kind] && <span className=\"sr-only select-none\">{SAID[row.kind]}</span>}\n                {/* An empty line holds a line break, so it keeps its height and a hand copy keeps the blank line. */}\n                {row.text === \"\" ? \"\\n\" : row.html === undefined ? row.text : <span dangerouslySetInnerHTML={{ __html: row.html }} />}\n              </span>\n            </span>\n          ))}\n        </code>\n      </pre>\n      <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 rounded-litt-surface shadow-[inset_0_0_0_1px_var(--litt-line)] in-[[data-material=flat]]:hidden\" />\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {state === \"copied\" ? \"Copied\" : state === \"failed\" ? \"Copy failed. Select the text and copy it by hand.\" : \"\"}\n      </span>\n    </div>\n  );\n}\n",
      "target": "components/litt/code-block.tsx"
    }
  ],
  "docs": "Import CodeBlock from components/litt/code-block. Pass the code, a lang (tsx, ts, jsx, js, diff, shell or text) and a filename for the header. The label names the scroll area and defaults to the file name. lineNumbers adds a gutter, wrap wraps long lines, and maxHeight (a number or a CSS length) makes a tall block scroll. A diff reads each line's first character as its sign. The syntax colors are declared on the block itself, so it needs no stylesheet. It imports sugar-high.",
  "meta": {
    "version": "2026.11.06",
    "released": "2026-11-06",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
