{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "type": "registry:ui",
  "title": "Code Block",
  "description": "Highlighted code with a sticky gutter, wrap that hangs its indent, and folding.",
  "categories": [
    "developer"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion",
    "sugar-high"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json",
    "https://www.stealth.pm/r/copy-button.json"
  ],
  "files": [
    {
      "path": "components/ui/code-block.tsx",
      "content": "\"use client\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { Tooltip } from \"@base-ui/react/tooltip\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { parse } from \"sugar-high/core\";\nimport { languages, type Language } from \"sugar-high/lang\";\nimport { CopyButton } from \"@/components/ui/copy-button\";\nimport { cn } from \"@/lib/cn\";\nimport { ChevronDown, Code, File } from \"@/lib/icons\";\nimport { ease } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\n/* -------------------------------------------------------------------------------------------------\n * Highlighting\n * -----------------------------------------------------------------------------------------------*/\n\nconst NAMES: Record<string, string> = {\n  javascript: \"JavaScript\", typescript: \"TypeScript\", css: \"CSS\", python: \"Python\", c: \"C\", go: \"Go\", java: \"Java\",\n  rust: \"Rust\", json: \"JSON\", diff: \"Diff\", shell: \"Shell\", cpp: \"C++\", csharp: \"C#\", sql: \"SQL\", html: \"HTML\",\n  yaml: \"YAML\", markdown: \"Markdown\", plaintext: \"Text\", ruby: \"Ruby\", kotlin: \"Kotlin\", swift: \"Swift\", php: \"PHP\",\n  toml: \"TOML\", powershell: \"PowerShell\", dockerfile: \"Dockerfile\", graphql: \"GraphQL\", hcl: \"HCL\", zig: \"Zig\", lua: \"Lua\",\n};\n\nconst find = (name?: string) => {\n  const n = name?.trim().toLowerCase().replace(/^\\./, \"\");\n  return n ? languages.find((l) => l.id === n || l.extension === n || l.aliases.includes(n)) : undefined;\n};\n\nexport type CodeLanguage = Language & { label: string };\n\n/** The language from an explicit name or alias, else the filename's extension, else plain text. */\nexport function resolveLanguage(lang?: string, filename?: string): CodeLanguage {\n  const base = filename?.split(\"/\").pop();\n  const l = find(lang) ?? find(base?.split(\".\").pop()) ?? find(base) ?? find(\"plaintext\")!;\n  return { ...l, label: NAMES[l.id] ?? l.id };\n}\n\nexport type CodeToken = { type: string; value: string };\n\n/** Splits code into lines of tokens. Pure, so it runs the same on the server and the client. */\nexport function tokenize(code: string, language: Language): CodeToken[][] {\n  // One trailing newline is the file ending, not an empty last line.\n  const text = code.replace(/\\r\\n?/g, \"\\n\").replace(/\\n$/, \"\");\n  const lines = parse(text, language.config).lines.map((l) => l.tokens.map(({ type, value }) => ({ type, value })));\n  return lines.length ? lines : [[]];\n}\n\n/**\n * Monochrome syntax colors, built only from the foreground steps: names you scan\n * for are brightest, structure (keywords, punctuation) recedes, comments sit\n * quietest. Every value is a --sh-* variable, so a product can retheme it.\n */\nexport const codeTheme = {\n  \"--sh-identifier\": \"color-mix(in oklab, var(--fg) 86%, var(--fg-2))\",\n  \"--sh-property\": \"color-mix(in oklab, var(--fg) 86%, var(--fg-2))\",\n  \"--sh-entity\": \"var(--fg)\",\n  \"--sh-class\": \"var(--fg)\",\n  \"--sh-jsxliterals\": \"var(--fg)\",\n  \"--sh-string\": \"var(--fg-2)\",\n  \"--sh-keyword\": \"color-mix(in oklab, var(--fg-2) 55%, var(--fg-3))\",\n  \"--sh-sign\": \"var(--fg-3)\",\n  \"--sh-comment\": \"var(--fg-3)\",\n} as React.CSSProperties;\n\nexport function Tokens({ tokens }: { tokens: CodeToken[] }) {\n  return tokens.map((t, i) =>\n    t.type === \"space\" ? (\n      t.value\n    ) : (\n      <span key={i} style={{ color: `var(--sh-${t.type})` }} className={t.type === \"comment\" ? \"italic\" : undefined}>\n        {t.value}\n      </span>\n    ),\n  );\n}\n\n/** Leading whitespace of a tokenized line, in columns (tabs count as two). */\nexport const indentOf = (tokens: CodeToken[]) => {\n  let n = 0;\n  for (const t of tokens) {\n    const m = t.value.match(/^[ \\t]*/)![0];\n    n += m.replace(/\\t/g, \"  \").length;\n    if (m.length < t.value.length) break;\n  }\n  return n;\n};\n\n/** Line numbers as a flat set from `[3, [8, 12]]`. */\nexport function lineSet(ranges: Array<number | [number, number]> = []) {\n  const set = new Set<number>();\n  for (const r of ranges) {\n    if (typeof r === \"number\") set.add(r);\n    else for (let n = Math.min(...r); n <= Math.max(...r); n++) set.add(n);\n  }\n  return set;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * CodeLines: the <code> itself, reusable anywhere a block of highlighted lines is needed\n * -----------------------------------------------------------------------------------------------*/\n\nexport type CodeLinesProps = Omit<React.ComponentProps<\"code\">, \"children\"> & {\n  lines: CodeToken[][];\n  lineNumbers?: boolean;\n  /** The number of the first line, for excerpts from the middle of a file. */\n  startLine?: number;\n  /** 1-based line numbers (relative to startLine) to mark. */\n  highlight?: Set<number>;\n  wrap?: boolean;\n  /** Reserve the gutter for at least this many digits, so sibling blocks (tabs, panes) line their code up. */\n  minDigits?: number;\n};\n\nexport function CodeLines({ lines, lineNumbers = true, startLine = 1, highlight, wrap = false, minDigits = 1, className, style, ...rest }: CodeLinesProps) {\n  const digits = Math.max(minDigits, String(startLine + lines.length - 1).length);\n  return (\n    <code\n      data-wrap={wrap || undefined}\n      style={{ ...codeTheme, \"--digits\": digits, ...style } as React.CSSProperties}\n      className={cn(\"grid font-mono text-[12.5px] leading-5\", wrap ? \"w-full\" : \"w-max min-w-full\", className)}\n      {...rest}\n    >\n      {lines.map((tokens, i) => {\n        const n = startLine + i;\n        const hl = highlight?.has(n);\n        return (\n          // Lines are blocks, so a selection copies with real line breaks and without the numbers.\n          <span\n            key={i}\n            data-line={n}\n            data-highlighted={hl || undefined}\n            className={cn(\n              \"group/line grid min-h-5\",\n              lineNumbers ? \"grid-cols-[auto_minmax(0,1fr)]\" : \"grid-cols-[minmax(0,1fr)]\",\n              // Opaque washes: the sticky gutter has to hide the code that scrolls under it.\n              \"[--line-bg:var(--raised)] data-highlighted:[--line-bg:color-mix(in_oklab,var(--fg)_6%,var(--raised))]\",\n              \"bg-(--line-bg)\",\n              !lineNumbers && \"data-highlighted:shadow-[inset_2px_0_var(--fg-3)]\",\n            )}\n          >\n            {lineNumbers && (\n              <span\n                aria-hidden\n                className={cn(\n                  \"sticky left-0 z-[1] select-none bg-(--line-bg) pl-4 pr-4 text-right tabular text-fg-4\",\n                  \"min-w-[calc(var(--digits)*1ch+2rem)] transition-colors duration-150 ease-out\",\n                  \"group-hover/line:text-fg-3 group-data-highlighted/line:text-fg-2 group-data-highlighted/line:shadow-[inset_2px_0_var(--fg-3)]\",\n                )}\n              >\n                {n}\n              </span>\n            )}\n            <span\n              className={cn(\"pr-4\", !lineNumbers && \"pl-4\", wrap ? \"whitespace-pre-wrap [overflow-wrap:anywhere]\" : \"whitespace-pre\")}\n              // A wrapped line hangs its continuation two columns past its own indent, so it still reads as one statement.\n              style={wrap ? { paddingInlineStart: `calc(${indentOf(tokens) + 2}ch + ${lineNumbers ? 0 : 16}px)`, textIndent: `-${indentOf(tokens) + 2}ch` } : undefined}\n            >\n              {/* An empty block copies as nothing; the break keeps blank lines in a copied selection. */}\n              {tokens.length ? <Tokens tokens={tokens} /> : <br />}\n            </span>\n          </span>\n        );\n      })}\n    </code>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Small shared pieces\n * -----------------------------------------------------------------------------------------------*/\n\nexport function Hint({ label, children }: { label: string; children: React.ReactElement }) {\n  return (\n    <Tooltip.Root>\n      <Tooltip.Trigger render={children} />\n      <Tooltip.Portal>\n        <Tooltip.Positioner side=\"top\" sideOffset={6} collisionPadding={8} className=\"z-(--z-tooltip)\">\n          <Tooltip.Popup\n            className={cn(\n              \"flex min-h-[26px] items-center rounded-lg border border-line-2 bg-raised px-2 py-[3px] text-[12px] leading-4 text-fg shadow-pop outline-none\",\n              \"origin-(--transform-origin) transition-[opacity,scale,translate] duration-150 ease-out-expo\",\n              \"data-starting-style:scale-96 data-starting-style:translate-y-0.5 data-starting-style:opacity-0\",\n              \"data-ending-style:opacity-0 data-ending-style:duration-100 data-instant:transition-none\",\n            )}\n          >\n            {label}\n          </Tooltip.Popup>\n        </Tooltip.Positioner>\n      </Tooltip.Portal>\n    </Tooltip.Root>\n  );\n}\n\n/** Marks an element that scrolls sideways with data-overflow and --fade-end, straight on the DOM so scrolling never re-renders. */\nexport function useSideScroll(ref: React.RefObject<HTMLElement | null>) {\n  const [overflows, setOverflows] = useState(false);\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    let frame = 0;\n    const update = () => {\n      cancelAnimationFrame(frame);\n      frame = requestAnimationFrame(() => {\n        const max = el.scrollWidth - el.clientWidth;\n        const more = max - el.scrollLeft > 1;\n        el.style.setProperty(\"--fade-end\", more ? \"32px\" : \"0px\");\n        el.toggleAttribute(\"data-scrolled\", el.scrollLeft > 1);\n        setOverflows(max > 1);\n      });\n    };\n    update();\n    el.addEventListener(\"scroll\", update, { passive: true });\n    const ro = new ResizeObserver(update);\n    ro.observe(el);\n    if (el.firstElementChild) ro.observe(el.firstElementChild);\n    return () => {\n      cancelAnimationFrame(frame);\n      el.removeEventListener(\"scroll\", update);\n      ro.disconnect();\n    };\n  }, [ref]);\n  return overflows;\n}\n\nconst iconButton = cn(\n  \"relative grid size-7 shrink-0 place-items-center rounded-md text-fg-3 outline-none\",\n  \"transition-[background-color,color,scale] duration-150 ease-out hover:bg-hover hover:text-fg active:scale-[0.92] active:duration-75\",\n  \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n  \"before:absolute before:-inset-2 before:content-[''] pointer-fine:before:hidden\",\n);\n\nfunction WrapIcon({ on }: { on: boolean }) {\n  const reduce = useReducedMotion();\n  return (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.4} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n      <path d=\"M2.75 4h10.5M2.75 12h3\" />\n      {/* The middle line folds back on itself when wrapping is on, and runs straight off the edge when it's off. */}\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        {on ? (\n          <motion.path\n            key=\"on\"\n            d=\"M2.75 8h8.25a2 2 0 0 1 0 4H8.5m1.5-1.5L8.5 12l1.5 1.5\"\n            initial={reduce ? { opacity: 0 } : { pathLength: 0, opacity: 0 }}\n            animate={{ pathLength: 1, opacity: 1 }}\n            exit={{ opacity: 0, transition: { duration: 0.08 } }}\n            transition={{ duration: reduce ? 0.12 : 0.34, ease: ease.out }}\n          />\n        ) : (\n          <motion.path\n            key=\"off\"\n            d=\"M2.75 8h10.5\"\n            initial={reduce ? { opacity: 0 } : { pathLength: 0.4, opacity: 0 }}\n            animate={{ pathLength: 1, opacity: 1 }}\n            exit={{ opacity: 0, transition: { duration: 0.08 } }}\n            transition={{ duration: reduce ? 0.12 : 0.24, ease: ease.out }}\n          />\n        )}\n      </AnimatePresence>\n    </svg>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * CodeBlock\n * -----------------------------------------------------------------------------------------------*/\n\nconst LINE = 20; // leading-5\nconst PAD = 12; // py-3\n\nexport type CodeBlockProps = Omit<React.ComponentProps<\"figure\">, \"children\"> & {\n  code: string;\n  /** Any language name, alias or extension (\"ts\", \"bash\", \"yml\"). Falls back to the filename's extension, then plain text. */\n  lang?: string;\n  /** Shown in the header. Directories truncate before the file name does. */\n  filename?: string;\n  lineNumbers?: boolean;\n  startLine?: number;\n  /** Lines to mark, as numbers or inclusive ranges: `[4, [9, 12]]`. */\n  highlightLines?: Array<number | [number, number]>;\n  wrap?: boolean;\n  defaultWrap?: boolean;\n  onWrapChange?: (wrap: boolean) => void;\n  /** Blocks longer than this collapse to it, with a control to show the rest. `false` never collapses. */\n  maxLines?: number | false;\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  /** Hide the copy button, for code that isn't meant to be run. */\n  copyable?: boolean;\n};\n\nexport function CodeBlock({\n  code,\n  lang,\n  filename,\n  lineNumbers = true,\n  startLine = 1,\n  highlightLines,\n  wrap: wrapProp,\n  defaultWrap = false,\n  onWrapChange,\n  maxLines = 16,\n  expanded: expandedProp,\n  defaultExpanded = false,\n  onExpandedChange,\n  copyable = true,\n  className,\n  ...rest\n}: CodeBlockProps) {\n  const reduce = useReducedMotion();\n  const id = useId();\n  const figure = useRef<HTMLElement>(null);\n  const scroller = useRef<HTMLPreElement>(null);\n  const language = useMemo(() => resolveLanguage(lang, filename), [lang, filename]);\n  const lines = useMemo(() => tokenize(code, language), [code, language]);\n  const highlight = useMemo(() => lineSet(highlightLines), [highlightLines]);\n  const [wrap, setWrap] = useControllableState({ value: wrapProp, defaultValue: defaultWrap, onChange: onWrapChange });\n  const [expanded, setExpanded] = useControllableState({ value: expandedProp, defaultValue: defaultExpanded, onChange: onExpandedChange });\n  const overflows = useSideScroll(scroller);\n\n  // Collapsing to hide two or three lines is more friction than it saves.\n  const collapsible = maxLines !== false && lines.length > maxLines + 4;\n  const collapsed = collapsible && !expanded;\n  const hidden = collapsible ? lines.length - maxLines : 0;\n\n  const slash = filename?.lastIndexOf(\"/\") ?? -1;\n  const dir = filename && slash >= 0 ? filename.slice(0, slash + 1) : \"\";\n  const base = filename ? filename.slice(slash + 1) : \"\";\n\n  const toggle = () => {\n    const next = !expanded;\n    setExpanded(next);\n    // Collapsing a long block can leave the reader below it. Bring its top back if it went off screen.\n    if (!next && figure.current && figure.current.getBoundingClientRect().top < 0) {\n      figure.current.scrollIntoView({ block: \"start\", behavior: reduce ? \"auto\" : \"smooth\" });\n    }\n  };\n\n  return (\n    <figure\n      ref={figure}\n      data-wrap={wrap || undefined}\n      data-state={collapsible ? (collapsed ? \"collapsed\" : \"expanded\") : undefined}\n      className={cn(\"@container relative m-0 flex min-w-0 flex-col overflow-hidden rounded-xl border border-line bg-raised shadow-[var(--shadow)]\", className)}\n      {...rest}\n    >\n      <Tooltip.Provider delay={500}>\n        <figcaption className=\"flex h-10 shrink-0 items-center gap-2 border-b border-line pl-3.5 pr-1.5\">\n          {filename ? (\n            <>\n              <File size={14} className=\"shrink-0 text-fg-3\" />\n              <span className=\"flex min-w-0 flex-1 font-mono text-[12px]\">\n                <span className=\"truncate text-fg-3\">{dir}</span>\n                <span className=\"shrink-0 text-fg\">{base}</span>\n              </span>\n              <span className=\"shrink-0 font-mono text-2xs uppercase tracking-[0.08em] text-fg-4 @max-[400px]:hidden\">{language.label}</span>\n            </>\n          ) : (\n            <>\n              <Code size={14} className=\"shrink-0 text-fg-3\" />\n              <span className=\"min-w-0 flex-1 truncate text-[12.5px] text-fg-2\">{language.label}</span>\n            </>\n          )}\n          <span className=\"ml-1 flex items-center gap-0.5\">\n            <Hint label={wrap ? \"Don’t wrap lines\" : \"Wrap lines\"}>\n              <Toggle pressed={wrap} onPressedChange={setWrap} aria-label=\"Wrap lines\" className={cn(iconButton, \"data-pressed:text-fg\")}>\n                <WrapIcon on={wrap} />\n              </Toggle>\n            </Hint>\n            {copyable && (\n              <Hint label=\"Copy code\">\n                <CopyButton value={code.replace(/\\n$/, \"\")} iconOnly variant=\"ghost\" size=\"sm\" label=\"Copy code\" />\n              </Hint>\n            )}\n          </span>\n        </figcaption>\n      </Tooltip.Provider>\n\n      <div\n        className={cn(\n          \"relative min-w-0\",\n          // The focus ring sits on top of the code: the rows' own backgrounds would paint over an outline on the scroller.\n          \"after:pointer-events-none after:absolute after:inset-0 after:z-[2] after:rounded-[inherit] after:content-['']\",\n          \"has-[pre:focus-visible]:after:shadow-[inset_0_0_0_1px_var(--fg-3)]\",\n        )}\n      >\n        <motion.pre\n          ref={scroller}\n          id={id}\n          // Only a block that actually scrolls sideways joins the tab order, so arrow keys can scroll it.\n          tabIndex={overflows && !wrap ? 0 : undefined}\n          aria-label={overflows && !wrap ? `${filename ?? language.label} code` : undefined}\n          initial={false}\n          animate={{ height: collapsed ? PAD + maxLines * LINE : \"auto\" }}\n          transition={reduce ? { duration: 0 } : { duration: 0.34, ease: ease.inOut }}\n          className={cn(\n            \"m-0 overflow-x-auto overflow-y-hidden overscroll-x-contain py-3 outline-none\",\n            \"[mask-image:linear-gradient(to_left,transparent,var(--fg)_var(--fade-end,0px))]\",\n          )}\n        >\n          <CodeLines lines={lines} lineNumbers={lineNumbers} startLine={startLine} highlight={highlight} wrap={wrap} />\n        </motion.pre>\n        {collapsible && (\n          // The cut edge: code fades out where it's hidden, so the block reads as \"more below\", not as the end.\n          <div\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-linear-to-b from-transparent to-raised\",\n              \"transition-opacity duration-300 ease-out\",\n              collapsed ? \"opacity-100\" : \"opacity-0\",\n            )}\n          />\n        )}\n      </div>\n\n      {collapsible && (\n        <div className=\"flex h-10 shrink-0 items-center border-t border-line px-1.5\">\n          <button\n            type=\"button\"\n            aria-expanded={!collapsed}\n            aria-controls={id}\n            onClick={toggle}\n            className={cn(\n              \"group/more relative inline-flex h-7 select-none items-center gap-1.5 rounded-md pl-1.5 pr-2 text-[12.5px] font-medium text-fg-2 outline-none\",\n              \"transition-[background-color,color,scale] duration-150 ease-out hover:bg-hover hover:text-fg active:scale-[0.97] active:duration-75\",\n              \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n              \"before:absolute before:-inset-y-2 before:inset-x-0 before:content-[''] pointer-fine:before:hidden\",\n            )}\n          >\n            <ChevronDown\n              size={14}\n              className={cn(\"text-fg-3 transition-transform duration-300 ease-in-out-quart group-hover/more:text-fg-2\", !collapsed && \"-rotate-180\")}\n            />\n            {/* Both labels share one cell, so the button keeps one width. */}\n            <span className=\"grid overflow-hidden text-left\">\n              <span aria-hidden className=\"invisible col-start-1 row-start-1\">Show {hidden} more lines</span>\n              <span aria-hidden className=\"invisible col-start-1 row-start-1\">Show less</span>\n              <AnimatePresence initial={false}>\n                <motion.span\n                  key={collapsed ? \"more\" : \"less\"}\n                  className=\"col-start-1 row-start-1 tabular\"\n                  initial={reduce ? { opacity: 0 } : { opacity: 0, y: collapsed ? -8 : 8, filter: \"blur(2px)\" }}\n                  animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n                  exit={reduce ? { opacity: 0 } : { opacity: 0, y: collapsed ? 8 : -8, filter: \"blur(2px)\", transition: { duration: 0.14 } }}\n                  transition={{ duration: reduce ? 0.12 : 0.22, ease: ease.out }}\n                >\n                  {collapsed ? `Show ${hidden} more lines` : \"Show less\"}\n                </motion.span>\n              </AnimatePresence>\n            </span>\n          </button>\n          <span className=\"ml-auto pr-2 font-mono text-2xs tabular text-fg-4\">{lines.length} lines</span>\n        </div>\n      )}\n    </figure>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/code-block"
}
