{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"code-block","type":"registry:ui","title":"Code Block","description":"Code display with a filename tab, line numbers, highlighted lines that sweep in, lightweight built-in token coloring and a copy button that springs into a check.","dependencies":["motion","lucide-react"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/code-block.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useMemo, useRef, useState, type CSSProperties, type HTMLAttributes } from \"react\";\nimport { AnimatePresence, MotionConfig, motion } from \"motion/react\";\nimport { Check, Copy, FileCode2 } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n// Highlighted lines sweep their tint in from the left, staggered by line.\nconst css = `\n@keyframes ui-code-hl{from{scale:0 1;opacity:0}}\n.ui-code-hl::before{content:\"\";position:absolute;inset:0;z-index:-1;transform-origin:left;background:color-mix(in oklab,var(--primary) 12%,transparent);box-shadow:inset 2px 0 var(--primary);animation:ui-code-hl .6s cubic-bezier(.6,0,.2,1) both;animation-delay:calc(var(--i)*40ms + .15s)}\n@media (prefers-reduced-motion:reduce){.ui-code-hl::before{animation:none}}\n`;\n\ntype TokenType = \"plain\" | \"comment\" | \"string\" | \"number\" | \"keyword\" | \"function\" | \"type\" | \"tag\" | \"attr\" | \"punct\";\ntype Token = [TokenType, string];\n\nconst tokenClass: Record<TokenType, string> = {\n  plain: \"\",\n  comment: \"text-muted-foreground italic\",\n  string: \"text-emerald-700 dark:text-emerald-300\",\n  number: \"text-amber-700 dark:text-amber-300\",\n  keyword: \"text-violet-600 dark:text-violet-300\",\n  function: \"text-sky-700 dark:text-sky-300\",\n  type: \"text-rose-600 dark:text-rose-300\",\n  tag: \"text-rose-600 dark:text-rose-300\",\n  attr: \"text-orange-700 dark:text-orange-300\",\n  punct: \"text-muted-foreground\",\n};\n\nconst KEYWORDS = new Set(\n  (\n    \"import export from default as const let var function return if else for while do switch case break continue new \" +\n    \"class extends interface type enum implements public private protected readonly static async await yield try catch \" +\n    \"finally throw typeof instanceof in of void delete this super true false null undefined keyof satisfies \" +\n    \"echo cd sudo npm npx pnpm yarn bun git def self None True False lambda pass with elif print\"\n  ).split(\" \"),\n);\n\n// One pass, one regex: comments | strings | numbers | JSX tag opener | identifiers | punctuation.\n// Good enough to color TS/JS/JSX, CSS, JSON, shell and Python readably; not a real parser.\nconst REST =\n  /(\"(?:\\\\.|[^\"\\\\\\n])*\"|'(?:\\\\.|[^'\\\\\\n])*'|`(?:\\\\.|[^`\\\\])*`)|(\\b\\d[\\d_]*(?:\\.\\d+)?(?:e[+-]?\\d+)?(?:px|rem|em|ms|s|%)?\\b)|(<\\/?[A-Za-z][\\w.-]*)|([A-Za-z_$][\\w$-]*)|([{}()[\\];,.:=+\\-*/%!?&|<>~^@#]+)/\n    .source;\nconst SLASH_RE = new RegExp(/(\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/|<!--[\\s\\S]*?-->)|/.source + REST, \"g\");\nconst HASH_RE = new RegExp(/((?<!\\S)#[^\\n]*)|/.source + REST, \"g\");\n\nfunction tokenize(code: string, language: string): Token[] {\n  const out: Token[] = [];\n  const hashComments = /^(bash|sh|shell|zsh|py|python|yaml|yml)$/.test(language);\n  const keyed = /^(css|scss|json|ya?ml)$/.test(language);\n  let last = 0;\n  for (const m of code.matchAll(hashComments ? HASH_RE : SLASH_RE)) {\n    const i = m.index;\n    if (i > last) out.push([\"plain\", code.slice(last, i)]);\n    const [text, comment, str, num, tag, ident] = m;\n    const after = code.slice(i + text.length);\n    let type: TokenType = \"punct\";\n    if (comment) type = \"comment\";\n    else if (str) type = keyed && /^\\s*:/.test(after) ? \"attr\" : \"string\";\n    else if (num) type = \"number\";\n    else if (tag) {\n      out.push([\"punct\", text.startsWith(\"</\") ? \"</\" : \"<\"]);\n      out.push([\"tag\", text.replace(/^<\\/?/, \"\")]);\n      last = i + text.length;\n      continue;\n    } else if (ident) {\n      if (KEYWORDS.has(ident)) type = \"keyword\";\n      else if (/^=[\"'{]/.test(after) || (keyed && /^\\s*:/.test(after))) type = \"attr\";\n      else if (/^\\s*\\(/.test(after)) type = \"function\";\n      else if (/^[A-Z]/.test(ident)) type = \"type\";\n      else type = \"plain\";\n    }\n    out.push([type, text]);\n    last = i + text.length;\n  }\n  if (last < code.length) out.push([\"plain\", code.slice(last)]);\n  return out;\n}\n\n/** Splits tokens at newlines so multi-line comments/strings still color each line. */\nfunction toLines(tokens: Token[]) {\n  const lines: Token[][] = [[]];\n  for (const [type, text] of tokens) {\n    text.split(\"\\n\").forEach((part, k) => {\n      if (k > 0) lines.push([]);\n      if (part) lines[lines.length - 1].push([type, part]);\n    });\n  }\n  return lines;\n}\n\n/** \"2,4-6\" or [2,4,5,6] -> Set of 1-based line numbers. */\nfunction parseLines(spec: string | number[] | undefined) {\n  const set = new Set<number>();\n  if (Array.isArray(spec)) spec.forEach((n) => set.add(n));\n  else\n    for (const part of (spec ?? \"\").split(\",\")) {\n      const [a, b = a] = part.split(\"-\").map((s) => parseInt(s, 10));\n      if (!Number.isNaN(a)) for (let n = a; n <= (Number.isNaN(b) ? a : b); n++) set.add(n);\n    }\n  return set;\n}\n\nexport interface CodeBlockProps extends Omit<HTMLAttributes<HTMLElement>, \"children\"> {\n  code: string;\n  /** Used for token rules and the header badge: tsx, ts, js, css, json, bash, python... */\n  language?: string;\n  /** Shown as a tab in the header. */\n  filename?: string;\n  showLineNumbers?: boolean;\n  /** Lines to highlight, 1-based: \"3\" | \"2,5-7\" | [2, 5, 6, 7]. */\n  highlightLines?: string | number[];\n  /** Show the copy button. */\n  copyable?: boolean;\n}\n\nexport function CodeBlock({\n  code,\n  language = \"tsx\",\n  filename,\n  showLineNumbers = true,\n  highlightLines,\n  copyable = true,\n  className,\n  ...props\n}: CodeBlockProps) {\n  const lines = useMemo(() => toLines(tokenize(code.replace(/\\n$/, \"\"), language)), [code, language]);\n  const marked = useMemo(() => parseLines(highlightLines), [highlightLines]);\n  const [copied, setCopied] = useState(false);\n  const timer = useRef<ReturnType<typeof setTimeout>>(undefined);\n  useEffect(() => () => clearTimeout(timer.current), []);\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(code);\n    } catch {\n      return; // clipboard blocked (insecure context / permissions): leave the icon as is\n    }\n    setCopied(true);\n    clearTimeout(timer.current);\n    timer.current = setTimeout(() => setCopied(false), 1800);\n  };\n\n  let hlIndex = 0;\n  return (\n    <figure\n      {...props}\n      className={cn(\n        \"group/code relative w-full overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-sm\",\n        className,\n      )}\n    >\n      <style href=\"ui-code-block\" precedence=\"default\">\n        {css}\n      </style>\n      <div className=\"flex h-11 items-center justify-between gap-2 border-b border-border bg-muted/40 pr-2\">\n        {filename ? (\n          <figcaption className=\"relative flex h-full items-center gap-2 border-r border-border bg-card px-4 text-xs font-medium text-foreground\">\n            <FileCode2 aria-hidden className=\"size-3.5 text-muted-foreground\" />\n            {filename}\n            <span aria-hidden className=\"absolute inset-x-0 -bottom-px h-px bg-card\" />\n            <span aria-hidden className=\"absolute inset-x-0 top-0 h-0.5 bg-primary\" />\n          </figcaption>\n        ) : (\n          <span className=\"px-4 text-xs font-medium tracking-wide text-muted-foreground uppercase\">{language}</span>\n        )}\n        <div className=\"flex items-center gap-2\">\n          {filename && (\n            <span className=\"rounded-md bg-muted px-1.5 py-0.5 font-mono text-[10px] tracking-wide text-muted-foreground uppercase\">\n              {language}\n            </span>\n          )}\n          {copyable && (\n            <MotionConfig reducedMotion=\"user\">\n              <button\n                type=\"button\"\n                onClick={copy}\n                aria-label={copied ? \"Copied\" : \"Copy code\"}\n                className={cn(\n                  \"relative grid size-8 place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60\",\n                  copied && \"text-emerald-600 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-400\",\n                )}\n              >\n                <AnimatePresence mode=\"popLayout\" initial={false}>\n                  <motion.span\n                    key={copied ? \"check\" : \"copy\"}\n                    initial={{ opacity: 0, scale: 0.4, rotate: copied ? -45 : 45 }}\n                    animate={{ opacity: 1, scale: 1, rotate: 0 }}\n                    exit={{ opacity: 0, scale: 0.4 }}\n                    transition={{ type: \"spring\", stiffness: 500, damping: 25 }}\n                    className=\"grid place-items-center\"\n                  >\n                    {copied ? <Check className=\"size-4\" strokeWidth={2.5} /> : <Copy className=\"size-4\" />}\n                  </motion.span>\n                </AnimatePresence>\n              </button>\n              <span aria-live=\"polite\" className=\"sr-only\">\n                {copied ? \"Copied to clipboard\" : \"\"}\n              </span>\n            </MotionConfig>\n          )}\n        </div>\n      </div>\n      <pre className=\"overflow-x-auto py-4 font-mono text-[13px] leading-6\" tabIndex={0}>\n        <code className=\"grid min-w-max\">\n          {lines.map((line, n) => {\n            const hl = marked.has(n + 1);\n            return (\n              <span\n                key={n}\n                data-highlighted={hl || undefined}\n                className={cn(\"relative isolate flex pr-6\", hl && \"ui-code-hl\", !showLineNumbers && \"pl-4\")}\n                style={hl ? ({ \"--i\": hlIndex++ } as CSSProperties) : undefined}\n              >\n                {showLineNumbers && (\n                  <span\n                    aria-hidden\n                    className={cn(\n                      \"w-12 shrink-0 pr-4 text-right text-muted-foreground/50 tabular-nums select-none\",\n                      hl && \"text-primary\",\n                    )}\n                  >\n                    {n + 1}\n                  </span>\n                )}\n                <span>\n                  {line.length ? line.map(([t, text], k) => (t === \"plain\" ? text : <span key={k} className={tokenClass[t]}>{text}</span>)) : \" \"}\n                </span>\n              </span>\n            );\n          })}\n        </code>\n      </pre>\n    </figure>\n  );\n}\n"}]}