{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "type": "registry:ui",
  "title": "Code Block",
  "description": "Syntax-highlighted code with a filename header, copy, line numbers, highlighted lines, diffs, wrap, collapsible height and streaming-safe rendering.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/icon-button.json",
    "https://kuro.abstergo.fyi/r/use-controllable-state.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/code-block/code-block.tsx",
      "type": "registry:ui",
      "target": "components/kura/code-block/code-block.tsx",
      "content": "\"use client\";\n\nimport { Check, ChevronsDownUp, ChevronsUpDown, Copy, FileCode2, WrapText } from \"lucide-react\";\nimport * as React from \"react\";\nimport { useControllableState } from \"@/hooks/kura/use-controllable-state\";\nimport { cn } from \"@/lib/utils\";\nimport { IconButton } from \"@/components/kura/icon-button\";\nimport { type CodeToken, type CodeTokenType, highlightCode } from \"./highlight\";\n\nexport interface CodeBlockProps extends Omit<React.ComponentProps<\"div\">, \"children\" | \"onCopy\"> {\n  /** The source to show. For diffs, pass unified-diff lines prefixed with `+`, `-` or a space. */\n  code: string;\n  /** Language id for highlighting and the header label, e.g. \"tsx\", \"python\", \"bash\", \"json\". */\n  language?: string;\n  /** File name shown in the header, e.g. \"app/page.tsx\". */\n  filename?: string;\n  /** Show a line-number gutter. Default false. */\n  lineNumbers?: boolean;\n  /** 1-based line numbers to emphasise, e.g. `[3, 4, 5]`. */\n  highlightLines?: readonly number[];\n  /** Treat `+` / `-` prefixed lines as additions and removals. Default true when language is \"diff\". */\n  diff?: boolean;\n  /** Code is still arriving: the unfinished last line stays unhighlighted until its newline lands, and a caret marks the live edge. */\n  streaming?: boolean;\n  /** Soft-wrap long lines (controlled). */\n  wrap?: boolean;\n  /** Initial wrap state when uncontrolled. Default false. */\n  defaultWrap?: boolean;\n  onWrapChange?: (wrap: boolean) => void;\n  /** Collapse taller code to this height in px, with a “Show all N lines” control. */\n  maxHeight?: number;\n  /** Expanded past `maxHeight` (controlled). */\n  expanded?: boolean;\n  /** Initial expanded state when uncontrolled. Default false. */\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  /** Show the copy button. Default true. */\n  copyable?: boolean;\n  /** Called with the copied text after a successful copy. */\n  onCopy?: (code: string) => void;\n  /** Show the filename/language header with its actions. Default true. */\n  header?: boolean;\n  /** Extra header actions, rendered before wrap and copy (e.g. an “Apply” button). */\n  actions?: React.ReactNode;\n}\n\ntype LineKind = \"context\" | \"add\" | \"remove\" | \"meta\";\n\ninterface Line {\n  tokens: CodeToken[];\n  kind: LineKind;\n  /** Unfinished trailing line while streaming. */\n  pending?: boolean;\n}\n\nconst tokenClass: Record<CodeTokenType, string | undefined> = {\n  plain: undefined,\n  keyword: \"text-(--kura-code-keyword)\",\n  string: \"text-(--kura-code-string)\",\n  comment: \"text-kura-fg-subtle italic\",\n  number: \"text-(--kura-code-constant)\",\n  constant: \"text-(--kura-code-constant)\",\n  function: \"text-(--kura-code-function)\",\n  type: \"text-(--kura-code-type)\",\n  tag: \"text-(--kura-code-tag)\",\n  attr: \"text-(--kura-code-attr)\",\n  property: \"text-(--kura-code-property)\",\n  variable: \"text-(--kura-code-constant)\",\n  punctuation: \"text-kura-fg-subtle\",\n  operator: \"text-kura-fg-muted\",\n};\n\n/*\n * Syntax colours are derived from --kura-fg-muted (so they keep its ≥ 4.5:1 contrast and flip\n * with the theme) by swapping in a hue. Override any of them with --kura-syntax-* on a parent.\n */\nconst syntax = (name: string, chroma: number, hue: number) =>\n  `var(--kura-syntax-${name}, oklch(from var(--kura-fg-muted) l ${chroma} ${hue}))`;\nconst syntaxVars = {\n  \"--kura-code-keyword\": syntax(\"keyword\", 0.13, 350),\n  \"--kura-code-string\": syntax(\"string\", 0.1, 150),\n  \"--kura-code-constant\": syntax(\"constant\", 0.11, 55),\n  \"--kura-code-function\": syntax(\"function\", 0.11, 255),\n  \"--kura-code-type\": syntax(\"type\", 0.09, 210),\n  \"--kura-code-tag\": syntax(\"tag\", 0.12, 25),\n  \"--kura-code-attr\": syntax(\"attr\", 0.1, 290),\n  \"--kura-code-property\": \"var(--kura-syntax-property, var(--kura-fg))\",\n} as React.CSSProperties;\n\nconst LINE_HEIGHT = 22;\n\nfunction useLines(code: string, language: string, diff: boolean, streaming: boolean): Line[] {\n  // While streaming, only complete lines are tokenized; the tail is plain text. Memoized on the\n  // stable prefix so new characters on the last line never re-colour earlier lines.\n  const cut = streaming ? code.lastIndexOf(\"\\n\") + 1 : code.length;\n  const stable = code.slice(0, cut);\n  const tail = code.slice(cut);\n\n  const stableLines = React.useMemo(() => {\n    if (!stable) return [] as Line[];\n    const body = streaming ? stable.slice(0, -1) : stable;\n    if (!diff) {\n      return highlightCode(body, language).map((tokens) => ({\n        tokens,\n        kind: \"context\" as LineKind,\n      }));\n    }\n    const raw = body.split(\"\\n\");\n    const kinds: LineKind[] = raw.map((l) =>\n      l.startsWith(\"+++\") || l.startsWith(\"---\") || l.startsWith(\"@@\")\n        ? \"meta\"\n        : l.startsWith(\"+\")\n          ? \"add\"\n          : l.startsWith(\"-\")\n            ? \"remove\"\n            : \"context\",\n    );\n    const lang = language === \"diff\" ? \"text\" : language;\n    const stripped = raw\n      .map((l, i) => (kinds[i] === \"meta\" ? \"\" : /^[+\\- ]/.test(l) ? l.slice(1) : l))\n      .join(\"\\n\");\n    const tokens = highlightCode(stripped, lang);\n    return raw.map((l, i) => ({\n      kind: kinds[i]!,\n      tokens: kinds[i] === \"meta\" ? [{ type: \"comment\" as const, text: l }] : (tokens[i] ?? []),\n    }));\n  }, [stable, language, diff, streaming]);\n\n  if (!streaming) return stableLines;\n  const pendingKind: LineKind = diff\n    ? tail.startsWith(\"+\")\n      ? \"add\"\n      : tail.startsWith(\"-\")\n        ? \"remove\"\n        : \"context\"\n    : \"context\";\n  const pendingText = diff && /^[+\\- ]/.test(tail) ? tail.slice(1) : tail;\n  return [\n    ...stableLines,\n    { tokens: [{ type: \"plain\", text: pendingText }], kind: pendingKind, pending: true },\n  ];\n}\n\n/**\n * Syntax-highlighted code with a filename/language header, copy with confirmation, line\n * numbers, highlighted lines, unified-diff rendering, soft-wrap toggle and a collapsible max\n * height. Highlighting is synchronous and dependency-free, and `streaming` keeps partially\n * received code from flickering.\n */\nexport function CodeBlock({\n  code,\n  language = \"text\",\n  filename,\n  lineNumbers = false,\n  highlightLines,\n  diff: diffProp,\n  streaming = false,\n  wrap: wrapProp,\n  defaultWrap = false,\n  onWrapChange,\n  maxHeight,\n  expanded: expandedProp,\n  defaultExpanded = false,\n  onExpandedChange,\n  copyable = true,\n  onCopy,\n  header = true,\n  actions,\n  className,\n  style,\n  ...props\n}: CodeBlockProps) {\n  const diff = diffProp ?? language === \"diff\";\n  const [wrap, setWrap] = useControllableState({\n    value: wrapProp,\n    defaultValue: defaultWrap,\n    onChange: onWrapChange,\n  });\n  const [expanded, setExpanded] = useControllableState({\n    value: expandedProp,\n    defaultValue: defaultExpanded,\n    onChange: onExpandedChange,\n  });\n  const lines = useLines(code, language, diff, streaming);\n  const highlighted = React.useMemo(() => new Set(highlightLines ?? []), [highlightLines]);\n\n  // Collapse: measure the natural height so expanding can transition to an exact max-height.\n  const contentRef = React.useRef<HTMLElement>(null);\n  const [fullHeight, setFullHeight] = React.useState<number | null>(null);\n  React.useLayoutEffect(() => {\n    const el = contentRef.current;\n    if (!el || maxHeight == null) return;\n    const measure = () => setFullHeight(el.scrollHeight);\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [maxHeight]);\n  const estimated = lines.length * LINE_HEIGHT + 24;\n  const natural = fullHeight || estimated;\n  const collapsible = maxHeight != null && natural > maxHeight + LINE_HEIGHT;\n  const collapsed = collapsible && !expanded;\n  const hiddenLines = collapsible\n    ? Math.max(0, lines.length - Math.floor((maxHeight - 12) / LINE_HEIGHT))\n    : 0;\n\n  // Copy with confirmation.\n  const [copied, setCopied] = React.useState(false);\n  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined);\n  React.useEffect(() => () => clearTimeout(timer.current), []);\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(code);\n      setCopied(true);\n      onCopy?.(code);\n      clearTimeout(timer.current);\n      timer.current = setTimeout(() => setCopied(false), 1600);\n    } catch {\n      // Clipboard can be unavailable (insecure context); the button simply does nothing.\n    }\n  };\n\n  const digits = Math.max(2, String(lines.length).length);\n  const label = filename ?? (language !== \"text\" ? language : \"Code\");\n\n  return (\n    <div\n      data-slot=\"code-block\"\n      data-language={language}\n      data-streaming={streaming || undefined}\n      data-wrap={wrap}\n      data-state={collapsible ? (expanded ? \"expanded\" : \"collapsed\") : undefined}\n      className={cn(\n        \"group/code relative flex flex-col overflow-hidden rounded-kura-xl bg-kura-surface-sunken font-kura-mono text-[13px] text-kura-fg shadow-kura-sm\",\n        className,\n      )}\n      style={{ ...syntaxVars, ...style }}\n      {...props}\n    >\n      {header && (\n        <div\n          data-slot=\"code-block-header\"\n          className=\"flex h-10 shrink-0 items-center gap-2 bg-kura-surface pr-1.5 pl-3.5 shadow-[inset_0_-1px_0_var(--kura-border-subtle)]\"\n        >\n          <span className=\"flex min-w-0 flex-1 items-center gap-2 font-kura-sans text-[13px] text-kura-fg-muted\">\n            {filename && (\n              <FileCode2 aria-hidden className=\"size-3.5 shrink-0 text-kura-fg-subtle\" />\n            )}\n            <span data-slot=\"code-block-title\" className=\"truncate\">\n              {filename ?? (language !== \"text\" ? language : \"\")}\n            </span>\n          </span>\n          {filename && language !== \"text\" && (\n            <span\n              data-slot=\"code-block-language\"\n              className=\"select-none px-1 text-[11px] text-kura-fg-subtle uppercase tracking-wide\"\n            >\n              {language}\n            </span>\n          )}\n          <span className=\"flex items-center gap-0.5\">\n            {actions}\n            {collapsible && (\n              <IconButton\n                size=\"sm\"\n                label={expanded ? \"Collapse code\" : \"Expand code\"}\n                aria-expanded={expanded}\n                icon={expanded ? <ChevronsDownUp /> : <ChevronsUpDown />}\n                onClick={() => setExpanded(!expanded)}\n              />\n            )}\n            <IconButton\n              size=\"sm\"\n              label={wrap ? \"Disable line wrap\" : \"Wrap lines\"}\n              aria-pressed={wrap}\n              icon={<WrapText />}\n              className=\"aria-pressed:bg-kura-active aria-pressed:text-kura-fg\"\n              onClick={() => setWrap(!wrap)}\n            />\n            {copyable && (\n              <IconButton\n                size=\"sm\"\n                label={copied ? \"Copied\" : \"Copy code\"}\n                data-copied={copied}\n                onClick={copy}\n                icon={\n                  <span className=\"relative grid size-3.5 place-items-center [&>svg]:col-start-1 [&>svg]:row-start-1\">\n                    <Copy\n                      className={cn(\n                        \"size-3.5 transition-[opacity,scale,filter] duration-(--kura-duration-fast) ease-kura-out motion-reduce:transition-opacity\",\n                        copied &&\n                          \"scale-75 opacity-0 blur-[2px] motion-reduce:scale-100 motion-reduce:blur-none\",\n                      )}\n                    />\n                    <Check\n                      className={cn(\n                        \"size-3.5 text-kura-success-text transition-[opacity,scale,filter] duration-(--kura-duration-fast) ease-kura-out motion-reduce:transition-opacity\",\n                        !copied &&\n                          \"scale-75 opacity-0 blur-[2px] motion-reduce:scale-100 motion-reduce:blur-none\",\n                      )}\n                    />\n                  </span>\n                }\n              />\n            )}\n          </span>\n          <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n            {copied ? \"Copied to clipboard\" : \"\"}\n          </span>\n        </div>\n      )}\n\n      <div\n        data-slot=\"code-block-body\"\n        className={cn(\n          \"relative\",\n          maxHeight != null &&\n            \"overflow-hidden transition-[max-height] duration-(--kura-duration-slow) ease-kura-out motion-reduce:transition-none\",\n        )}\n        style={\n          maxHeight != null\n            ? { maxHeight: collapsed ? maxHeight : collapsible ? natural : undefined }\n            : undefined\n        }\n      >\n        <section\n          ref={contentRef}\n          aria-label={`${label} code`}\n          // biome-ignore lint/a11y/noNoninteractiveTabindex: scrollable code must be keyboard-scrollable\n          tabIndex={0}\n          className={cn(\n            \"overflow-x-auto py-3 leading-[22px] outline-none [scrollbar-width:thin]\",\n            \"focus-visible:shadow-[inset_0_0_0_2px_var(--kura-ring)]\",\n          )}\n        >\n          <code\n            data-slot=\"code-block-code\"\n            className={cn(\"block\", wrap ? \"w-full\" : \"w-max min-w-full\")}\n          >\n            {lines.map((line, index) => {\n              const n = index + 1;\n              const isHighlighted = highlighted.has(n);\n              const last = index === lines.length - 1;\n              return (\n                <span\n                  key={index}\n                  data-slot=\"code-block-line\"\n                  data-line={n}\n                  data-highlighted={isHighlighted || undefined}\n                  data-diff={diff ? line.kind : undefined}\n                  className={cn(\n                    \"flex px-4\",\n                    isHighlighted &&\n                      \"bg-kura-hover shadow-[inset_2px_0_0_var(--kura-border-strong)]\",\n                    line.kind === \"add\" &&\n                      \"bg-kura-success/10 shadow-[inset_2px_0_0_var(--kura-success)]\",\n                    line.kind === \"remove\" &&\n                      \"bg-kura-danger/10 shadow-[inset_2px_0_0_var(--kura-danger)]\",\n                  )}\n                >\n                  {lineNumbers && (\n                    <span\n                      aria-hidden\n                      data-slot=\"code-block-line-number\"\n                      className=\"mr-4 shrink-0 select-none text-right text-kura-fg-subtle tabular-nums opacity-70\"\n                      style={{ width: `${digits}ch` }}\n                    >\n                      {n}\n                    </span>\n                  )}\n                  {diff && (\n                    <span\n                      aria-hidden\n                      className={cn(\n                        \"mr-3 w-[1ch] shrink-0 select-none\",\n                        line.kind === \"add\" && \"text-kura-success-text\",\n                        line.kind === \"remove\" && \"text-kura-danger-text\",\n                      )}\n                    >\n                      {line.kind === \"add\" ? \"+\" : line.kind === \"remove\" ? \"−\" : \" \"}\n                    </span>\n                  )}\n                  {diff && (line.kind === \"add\" || line.kind === \"remove\") && (\n                    <span className=\"sr-only\">{line.kind === \"add\" ? \"added: \" : \"removed: \"}</span>\n                  )}\n                  <span\n                    className={cn(\n                      \"min-w-0 flex-1\",\n                      wrap ? \"whitespace-pre-wrap break-words\" : \"whitespace-pre\",\n                      line.kind === \"remove\" && \"opacity-75\",\n                      line.pending && \"text-kura-fg-muted\",\n                    )}\n                  >\n                    {line.tokens.length === 0 && !line.pending\n                      ? \"​\"\n                      : line.tokens.map((t, i) => (\n                          <span key={i} className={tokenClass[t.type]}>\n                            {t.text}\n                          </span>\n                        ))}\n                    {streaming && last && (\n                      <span\n                        aria-hidden\n                        data-slot=\"code-block-caret\"\n                        className=\"ml-px inline-block h-[1.15em] w-[0.55em] translate-y-[0.2em] rounded-[2px] bg-kura-accent/70 animate-kura-pulse motion-reduce:animate-none\"\n                      />\n                    )}\n                  </span>\n                </span>\n              );\n            })}\n          </code>\n        </section>\n\n        {collapsed && (\n          <div\n            data-slot=\"code-block-expand\"\n            className=\"pointer-events-none absolute inset-x-0 bottom-0 flex h-20 items-end justify-center bg-linear-to-t from-kura-surface-sunken via-kura-surface-sunken/80 to-transparent pb-3\"\n          >\n            <button\n              type=\"button\"\n              aria-expanded={false}\n              onClick={() => setExpanded(true)}\n              className={cn(\n                \"pointer-events-auto inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-full bg-kura-surface-raised px-3 font-kura-sans text-kura-fg-muted text-xs shadow-kura-md outline-none\",\n                \"transition-[color,transform,box-shadow] duration-(--kura-duration-fast) ease-kura-out hover:text-kura-fg focus-visible:shadow-kura-focus active:scale-[0.97] motion-reduce:active:scale-100\",\n              )}\n            >\n              <ChevronsUpDown aria-hidden className=\"size-3\" />\n              Show {hiddenLines} more {hiddenLines === 1 ? \"line\" : \"lines\"}\n            </button>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/kura/code-block/highlight.ts",
      "type": "registry:ui",
      "target": "components/kura/code-block/highlight.ts",
      "content": "/**\n * A small, synchronous, zero-dependency syntax tokenizer. It is not a full grammar engine like\n * shiki; it is a careful set of sticky regexes per language family that gets the common cases\n * right, runs in microseconds, and — being synchronous — never flashes unhighlighted code.\n */\n\nexport type CodeTokenType =\n  | \"plain\"\n  | \"keyword\"\n  | \"string\"\n  | \"comment\"\n  | \"number\"\n  | \"constant\"\n  | \"function\"\n  | \"type\"\n  | \"tag\"\n  | \"attr\"\n  | \"property\"\n  | \"variable\"\n  | \"punctuation\"\n  | \"operator\";\n\nexport interface CodeToken {\n  type: CodeTokenType;\n  text: string;\n}\n\ntype Rule = readonly [CodeTokenType, RegExp];\n\nconst kw = (words: string, flags = \"y\") => new RegExp(`\\\\b(?:${words})\\\\b`, flags);\n\nconst DQ = /\"(?:\\\\.|[^\"\\\\\\n])*\"?/y;\nconst SQ = /'(?:\\\\.|[^'\\\\\\n])*'?/y;\nconst NUM = /\\b(?:0[xXbBoO][\\da-fA-F_]+|\\d[\\d_]*(?:\\.\\d+)?(?:[eE][+-]?\\d+)?n?)\\b/y;\nconst IDENT = /[A-Za-z_$][\\w$]*/y;\nconst CALL = /[A-Za-z_$][\\w$]*(?=\\s*\\()/y;\nconst TYPE = /\\b[A-Z][\\w$]*\\b/y;\nconst PROP = /(?<=\\.)[A-Za-z_$][\\w$]*/y;\nconst PUNCT = /[{}()[\\];,.]/y;\nconst OP = /=>|[=+\\-*/%!&|^~?:<>]+/y;\n\nconst JS_KEYWORDS =\n  \"abstract|as|async|await|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|infer|instanceof|interface|is|keyof|let|namespace|new|of|private|protected|public|readonly|return|satisfies|set|static|super|switch|this|throw|try|type|typeof|var|void|while|with|yield\";\n\nconst js: Rule[] = [\n  [\"comment\", /\\/\\/[^\\n]*/y],\n  [\"comment\", /\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y],\n  [\"string\", /`(?:\\\\[\\s\\S]|[^\\\\`])*`?/y],\n  [\"string\", DQ],\n  [\"string\", SQ],\n  [\"tag\", /(?<=<\\/?)[A-Za-z][\\w.]*/y],\n  [\"attr\", /[A-Za-z_$][\\w$-]*(?==(?!=|>))/y],\n  [\"constant\", kw(\"true|false|null|undefined|NaN|Infinity\")],\n  [\"keyword\", kw(JS_KEYWORDS)],\n  [\"number\", NUM],\n  [\"function\", CALL],\n  [\"type\", TYPE],\n  [\"property\", PROP],\n  [\"plain\", IDENT],\n  [\"punctuation\", PUNCT],\n  [\"operator\", OP],\n];\n\nconst json: Rule[] = [\n  [\"property\", /\"(?:\\\\.|[^\"\\\\\\n])*\"(?=\\s*:)/y],\n  [\"string\", DQ],\n  [\"number\", /-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/y],\n  [\"constant\", kw(\"true|false|null\")],\n  [\"punctuation\", /[{}[\\],:]/y],\n];\n\nconst bash: Rule[] = [\n  [\"comment\", /(?<=^|\\s)#[^\\n]*/my],\n  [\"string\", /\"(?:\\\\[\\s\\S]|[^\"\\\\])*\"?/y],\n  [\"string\", /'[^']*'?/y],\n  [\"variable\", /\\$(?:\\{[^}\\n]*\\}|\\w+|[@#?$!*0-9])/y],\n  [\n    \"keyword\",\n    kw(\"if|then|else|elif|fi|for|while|until|do|done|in|case|esac|function|export|local|return\"),\n  ],\n  [\"function\", /(?<=(?:^|[|;&(]|&&|\\|\\|)[ \\t]*(?:sudo[ \\t]+)?)[A-Za-z_.][\\w./-]*/my],\n  [\"attr\", /(?<=\\s)--?[A-Za-z][\\w-]*/y],\n  [\"number\", /\\b\\d+\\b/y],\n  [\"operator\", /[|&;<>]+|=/y],\n];\n\nconst python: Rule[] = [\n  [\"comment\", /#[^\\n]*/y],\n  [\n    \"string\",\n    /[rbfuRBFU]{0,2}(?:\"\"\"[\\s\\S]*?(?:\"\"\"|$)|'''[\\s\\S]*?(?:'''|$)|\"(?:\\\\.|[^\"\\\\\\n])*\"?|'(?:\\\\.|[^'\\\\\\n])*'?)/y,\n  ],\n  [\"attr\", /@[\\w.]+/y],\n  [\"constant\", kw(\"True|False|None\")],\n  [\n    \"keyword\",\n    kw(\n      \"and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield|match|case|self\",\n    ),\n  ],\n  [\"number\", NUM],\n  [\"function\", /(?<=\\bdef\\s+)\\w+/y],\n  [\"function\", CALL],\n  [\"type\", TYPE],\n  [\"property\", PROP],\n  [\"plain\", IDENT],\n  [\"punctuation\", PUNCT],\n  [\"operator\", OP],\n];\n\nconst css: Rule[] = [\n  [\"comment\", /\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y],\n  [\"keyword\", /@[\\w-]+/y],\n  [\"string\", DQ],\n  [\"string\", SQ],\n  [\"variable\", /--[\\w-]+/y],\n  [\"number\", /#[\\da-fA-F]{3,8}\\b/y],\n  [\"property\", /(?<=[{;\\s])[a-z-]+(?=\\s*:[^:])/y],\n  [\"function\", /[\\w-]+(?=\\()/y],\n  [\"number\", /-?(?:\\d*\\.)?\\d+(?:px|rem|em|%|ms|s|vh|vw|dvh|deg|fr|ch)?\\b%?/y],\n  [\"tag\", /[.#][\\w-]+|&/y],\n  [\"constant\", /!important/y],\n  [\"plain\", /[\\w-]+/y],\n  [\"punctuation\", /[{}();,:]/y],\n];\n\nconst markup: Rule[] = [\n  [\"comment\", /<!--[\\s\\S]*?(?:-->|$)/y],\n  [\"tag\", /(?<=<\\/?)[\\w:-]+/y],\n  [\"attr\", /[\\w:@.-]+(?==)/y],\n  [\"string\", DQ],\n  [\"string\", SQ],\n  [\"punctuation\", /<\\/?|\\/?>|=/y],\n];\n\nconst yaml: Rule[] = [\n  [\"comment\", /(?<=^|\\s)#[^\\n]*/my],\n  [\"property\", /(?<=^[ \\t]*(?:- )?)[\\w.$-]+(?=\\s*:(?:\\s|$))/my],\n  [\"string\", DQ],\n  [\"string\", SQ],\n  [\"constant\", kw(\"true|false|null|yes|no|on|off\")],\n  [\"number\", /-?\\b\\d+(?:\\.\\d+)?\\b/y],\n  [\"punctuation\", /[-:[\\]{},|>]/y],\n];\n\nconst sql: Rule[] = [\n  [\"comment\", /--[^\\n]*/y],\n  [\"comment\", /\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y],\n  [\"string\", SQ],\n  [\"string\", DQ],\n  [\n    \"keyword\",\n    kw(\n      \"select|from|where|and|or|not|insert|into|values|update|set|delete|create|table|index|drop|alter|add|join|left|right|inner|outer|on|group|by|order|having|limit|offset|as|distinct|union|all|case|when|then|else|end|in|is|null|primary|key|references|default|returning|with|asc|desc|exists|like\",\n      \"iy\",\n    ),\n  ],\n  [\"number\", NUM],\n  [\"function\", CALL],\n  [\"plain\", /\\w+/y],\n  [\"punctuation\", /[(),;.*]/y],\n  [\"operator\", /[=<>!+\\-/%]+/y],\n];\n\nfunction cLike(keywords: string, extra: Rule[] = []): Rule[] {\n  return [\n    [\"comment\", /\\/\\/[^\\n]*/y],\n    [\"comment\", /\\/\\*[\\s\\S]*?(?:\\*\\/|$)/y],\n    ...extra,\n    [\"string\", /`[^`]*`?/y],\n    [\"string\", DQ],\n    [\"string\", SQ],\n    [\"constant\", kw(\"true|false|nil|null|None|Some|Ok|Err|iota\")],\n    [\"keyword\", kw(keywords)],\n    [\"number\", NUM],\n    [\"function\", /[A-Za-z_][\\w]*!?(?=\\s*\\()/y],\n    [\"type\", TYPE],\n    [\"property\", PROP],\n    [\"plain\", IDENT],\n    [\"punctuation\", PUNCT],\n    [\"operator\", OP],\n  ];\n}\n\nconst go = cLike(\n  \"break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var|string|int|int64|float64|bool|error|byte|rune|any\",\n);\nconst rust = cLike(\n  \"as|async|await|break|const|continue|crate|dyn|else|enum|extern|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|Self|static|struct|super|trait|type|unsafe|use|where|while|i32|i64|u8|u32|u64|usize|f64|bool|str|String|Vec|Option|Result\",\n  [[\"attr\", /#!?\\[[^\\]\\n]*\\]/y]],\n);\nconst generic = cLike(\n  \"abstract|break|case|catch|class|const|continue|default|do|else|enum|extends|final|finally|for|if|implements|import|interface|new|package|private|protected|public|return|static|struct|switch|this|throw|throws|try|void|while|fun|val|var|let|func|def|end\",\n);\n\nconst LANGUAGES: Record<string, Rule[]> = {\n  js,\n  javascript: js,\n  jsx: js,\n  mjs: js,\n  cjs: js,\n  ts: js,\n  typescript: js,\n  tsx: js,\n  json,\n  jsonc: js,\n  json5: js,\n  bash,\n  sh: bash,\n  shell: bash,\n  zsh: bash,\n  console: bash,\n  python: python,\n  py: python,\n  css,\n  scss: css,\n  html: markup,\n  xml: markup,\n  svg: markup,\n  vue: markup,\n  yaml,\n  yml: yaml,\n  toml: yaml,\n  sql,\n  go,\n  rust,\n  rs: rust,\n  java: generic,\n  kotlin: generic,\n  swift: generic,\n  c: generic,\n  cpp: generic,\n  csharp: generic,\n  cs: generic,\n  php: generic,\n  ruby: generic,\n};\n\n/** Languages with a dedicated tokenizer (aliases included). Anything else renders as plain text. */\nexport const supportedLanguages = Object.keys(LANGUAGES);\n\n/** Tokenize `code` and split the result into lines (tokens never span a newline). */\nexport function highlightCode(code: string, language = \"text\"): CodeToken[][] {\n  const rules = LANGUAGES[language.toLowerCase()];\n  const lines: CodeToken[][] = [[]];\n  const push = (type: CodeTokenType, text: string) => {\n    const parts = text.split(\"\\n\");\n    parts.forEach((part, i) => {\n      if (i > 0) lines.push([]);\n      if (!part) return;\n      const line = lines[lines.length - 1]!;\n      const last = line[line.length - 1];\n      if (last && last.type === type) last.text += part;\n      else line.push({ type, text: part });\n    });\n  };\n  if (!rules) {\n    push(\"plain\", code);\n    return lines;\n  }\n  let i = 0;\n  outer: while (i < code.length) {\n    for (const [type, re] of rules) {\n      re.lastIndex = i;\n      const m = re.exec(code);\n      if (m && m[0].length > 0) {\n        push(type, m[0]);\n        i += m[0].length;\n        continue outer;\n      }\n    }\n    push(\"plain\", code[i]!);\n    i++;\n  }\n  return lines;\n}\n"
    },
    {
      "path": "registry/kura/code-block/index.ts",
      "type": "registry:ui",
      "target": "components/kura/code-block/index.ts",
      "content": "export { CodeBlock, type CodeBlockProps } from \"./code-block\";\nexport { type CodeToken, type CodeTokenType, highlightCode, supportedLanguages } from \"./highlight\";\n"
    }
  ],
  "categories": [
    "conversation",
    "code",
    "syntax-highlighting",
    "diff",
    "copy"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/code-block · Markdown for agents: https://kuro.abstergo.fyi/components/code-block.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "Show code a model wrote in a chat answer, including while it is still streaming.",
        "Show a file or snippet with a filename header and a copy button (docs, READMEs, agent output).",
        "Show an agent's proposed edit as a unified diff with +/- lines.",
        "Long files that should start collapsed to a fixed height with a “Show N more lines” control.",
        "Terminal commands (language=\"bash\") with an extra header action such as Run."
      ],
      "whenNotToUse": [
        "Inline code inside a sentence — use a plain <code> element.",
        "Tool arguments/results JSON inside an agent step — use tool-call, which pretty-prints JSON for you.",
        "Prose output from a model — use streaming-text or message.",
        "Editable code — use a real editor (CodeMirror/Monaco); CodeBlock is read-only."
      ]
    }
  }
}
