{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "code-block.tsx",
      "content": "\"use client\";\n\nimport { Check, Copy } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useMemo, useRef, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst COPY_RESET_MS = 2000;\n\n/** Transform-only spring for the copy icon swap — stays off the layout thread. */\nconst ICON_SPRING = {\n  type: \"spring\",\n  stiffness: 600,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\ntype TokenKind =\n  | \"call\"\n  | \"comment\"\n  | \"keyword\"\n  | \"number\"\n  | \"operator\"\n  | \"plain\"\n  | \"property\"\n  | \"punctuation\"\n  | \"string\"\n  | \"tag\"\n  | \"type\";\n\ntype Token = {\n  kind: TokenKind;\n  value: string;\n};\n\nconst KEYWORDS = new Set([\n  \"abstract\",\n  \"as\",\n  \"async\",\n  \"await\",\n  \"break\",\n  \"case\",\n  \"catch\",\n  \"class\",\n  \"const\",\n  \"continue\",\n  \"def\",\n  \"default\",\n  \"delete\",\n  \"do\",\n  \"elif\",\n  \"else\",\n  \"enum\",\n  \"export\",\n  \"extends\",\n  \"false\",\n  \"finally\",\n  \"fn\",\n  \"for\",\n  \"from\",\n  \"function\",\n  \"if\",\n  \"implements\",\n  \"import\",\n  \"in\",\n  \"instanceof\",\n  \"interface\",\n  \"let\",\n  \"match\",\n  \"new\",\n  \"None\",\n  \"not\",\n  \"null\",\n  \"of\",\n  \"pass\",\n  \"private\",\n  \"protected\",\n  \"public\",\n  \"readonly\",\n  \"return\",\n  \"satisfies\",\n  \"static\",\n  \"struct\",\n  \"switch\",\n  \"this\",\n  \"throw\",\n  \"true\",\n  \"try\",\n  \"type\",\n  \"typeof\",\n  \"undefined\",\n  \"var\",\n  \"void\",\n  \"while\",\n  \"yield\",\n]);\n\nconst WHITESPACE_RE = /^\\s+/;\nconst BLOCK_COMMENT_RE = /^\\/\\*[\\s\\S]*?(?:\\*\\/|$)/;\nconst LINE_COMMENT_RE = /^(?:\\/\\/|#)[^\\n]*/;\nconst TEMPLATE_STRING_RE = /^`(?:\\\\.|[\\s\\S])*?`/;\nconst DOUBLE_QUOTED_STRING_RE = /^\"(?:\\\\.|[^\"\\\\])*\"/;\nconst SINGLE_QUOTED_STRING_RE = /^'(?:\\\\.|[^'\\\\])*'/;\nconst TAG_RE = /^<\\/?[A-Za-z][\\w.-]*|^\\/?>/;\nconst ATTRIBUTE_RE = /^[A-Za-z_$][\\w$-]*(?==)/;\nconst PROPERTY_RE = /^[A-Za-z_$][\\w$-]*(?=\\s*:)/;\nconst IDENTIFIER_RE = /^[A-Za-z_$][\\w$]*/;\nconst TYPE_IDENTIFIER_RE = /^[A-Z]/;\nconst NUMBER_RE = /^-?\\d+(?:\\.\\d+)?/;\nconst OPERATOR_RE =\n  /^(?:=>|===|!==|==|!=|<=|>=|\\+\\+|--|\\|\\||&&|\\?\\?|\\.\\.\\.|[=<>+\\-*/%!?&|])/;\nconst PUNCTUATION_RE = /^[{}()[\\],.;:]/;\n\nconst STATIC_MATCHERS: { kind: TokenKind; regex: RegExp }[] = [\n  { kind: \"plain\", regex: WHITESPACE_RE },\n  { kind: \"comment\", regex: BLOCK_COMMENT_RE },\n  { kind: \"comment\", regex: LINE_COMMENT_RE },\n  { kind: \"string\", regex: TEMPLATE_STRING_RE },\n  { kind: \"string\", regex: DOUBLE_QUOTED_STRING_RE },\n  { kind: \"string\", regex: SINGLE_QUOTED_STRING_RE },\n  { kind: \"tag\", regex: TAG_RE },\n  { kind: \"property\", regex: ATTRIBUTE_RE },\n  { kind: \"property\", regex: PROPERTY_RE },\n  { kind: \"number\", regex: NUMBER_RE },\n  { kind: \"operator\", regex: OPERATOR_RE },\n  { kind: \"punctuation\", regex: PUNCTUATION_RE },\n];\n\n/** Theme-aware token palette — light colors on white, brighter ones in dark. */\nconst TOKEN_CLASS_NAMES: Record<TokenKind, string> = {\n  plain: \"text-zinc-800 dark:text-zinc-200\",\n  comment: \"text-zinc-400 italic dark:text-zinc-500\",\n  keyword: \"text-rose-600 dark:text-rose-400\",\n  string: \"text-emerald-600 dark:text-emerald-400\",\n  number: \"text-amber-600 dark:text-amber-400\",\n  property: \"text-sky-600 dark:text-sky-400\",\n  call: \"text-violet-600 dark:text-violet-400\",\n  type: \"text-indigo-600 dark:text-indigo-400\",\n  tag: \"text-teal-600 dark:text-teal-400\",\n  operator: \"text-zinc-500 dark:text-zinc-400\",\n  punctuation: \"text-zinc-400 dark:text-zinc-500\",\n};\n\nfunction readStaticToken(source: string): Token | null {\n  for (const matcher of STATIC_MATCHERS) {\n    const value = matcher.regex.exec(source)?.[0];\n\n    if (value) {\n      return { kind: matcher.kind, value };\n    }\n  }\n\n  return null;\n}\n\nfunction identifierKind(identifier: string, source: string): TokenKind {\n  if (KEYWORDS.has(identifier)) {\n    return \"keyword\";\n  }\n\n  if (TYPE_IDENTIFIER_RE.test(identifier)) {\n    return \"type\";\n  }\n\n  if (source.startsWith(`${identifier}(`)) {\n    return \"call\";\n  }\n\n  return \"plain\";\n}\n\nfunction readDynamicToken(source: string): Token {\n  const identifier = IDENTIFIER_RE.exec(source)?.[0];\n\n  if (identifier) {\n    return { kind: identifierKind(identifier, source), value: identifier };\n  }\n\n  return { kind: \"plain\", value: source[0] ?? \"\" };\n}\n\nfunction tokenizeLine(line: string): Token[] {\n  const tokens: Token[] = [];\n  let cursor = 0;\n\n  while (cursor < line.length) {\n    const source = line.slice(cursor);\n    const token = readStaticToken(source) ?? readDynamicToken(source);\n\n    tokens.push(token);\n    cursor += token.value.length;\n  }\n\n  return tokens;\n}\n\nfunction CopyButton({\n  copied,\n  onCopy,\n  reducedMotion,\n  floating = false,\n}: {\n  copied: boolean;\n  onCopy: () => void;\n  reducedMotion: boolean;\n  /** Glass-chip styling for when the button floats over the code area. */\n  floating?: boolean;\n}) {\n  const iconTransition = reducedMotion ? { duration: 0 } : ICON_SPRING;\n\n  return (\n    <motion.button\n      aria-label={copied ? \"Copied\" : \"Copy code\"}\n      className={cn(\n        \"flex h-7 items-center gap-1.5 rounded-md px-2 font-medium text-xs outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-ring/60\",\n        floating &&\n          \"border border-border/70 bg-background/85 shadow-sm backdrop-blur-sm\",\n        copied\n          ? \"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400\"\n          : \"text-muted-foreground hover:bg-foreground/[0.06] hover:text-foreground\"\n      )}\n      data-slot=\"code-block-copy\"\n      onClick={onCopy}\n      type=\"button\"\n      whileTap={reducedMotion ? undefined : { scale: 0.96 }}\n    >\n      <span className=\"relative size-3.5\">\n        <AnimatePresence initial={false}>\n          {copied ? (\n            <motion.span\n              animate={{ opacity: 1, scale: 1 }}\n              className=\"absolute inset-0 flex items-center justify-center\"\n              exit={{ opacity: 0, scale: 0.95 }}\n              initial={{ opacity: 0, scale: 0.95 }}\n              key=\"check\"\n              transition={iconTransition}\n            >\n              <Check className=\"size-3.5\" strokeWidth={2.5} />\n            </motion.span>\n          ) : (\n            <motion.span\n              animate={{ opacity: 1, scale: 1 }}\n              className=\"absolute inset-0 flex items-center justify-center\"\n              exit={{ opacity: 0, scale: 0.95 }}\n              initial={{ opacity: 0, scale: 0.95 }}\n              key=\"copy\"\n              transition={iconTransition}\n            >\n              <Copy className=\"size-3.5\" />\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </span>\n\n      {/* Both labels overlay a fixed-width slot, so the swap never shifts layout. */}\n      <span className=\"relative\">\n        <span aria-hidden className=\"invisible\">\n          Copied\n        </span>\n        <span\n          aria-hidden\n          className={cn(\n            \"absolute inset-0 text-left transition-opacity duration-200\",\n            copied ? \"opacity-0\" : \"opacity-100\"\n          )}\n        >\n          Copy\n        </span>\n        <span\n          aria-hidden\n          className={cn(\n            \"absolute inset-0 text-left transition-opacity duration-200\",\n            copied ? \"opacity-100\" : \"opacity-0\"\n          )}\n        >\n          Copied\n        </span>\n      </span>\n    </motion.button>\n  );\n}\n\nexport type CodeBlockProps = {\n  /** Source code to render. Leading and trailing blank lines are trimmed. */\n  code: string;\n  /** Language shown in the bottom status bar and used for the tab accent dot. */\n  language?: string;\n  /** Filename shown inside the editor-style tab at the top. */\n  filename?: string;\n  /** Show the line-number gutter. */\n  showLineNumbers?: boolean;\n  /** 1-based line numbers to emphasize with a tinted row and accent bar. */\n  highlightLines?: number[];\n  /** Max body height before the code scrolls, e.g. 320 or \"20rem\". */\n  maxHeight?: number | string;\n  /** Called with the code after it is written to the clipboard. */\n  onCopy?: (code: string) => void;\n  /** Extra classes for the outer shell. */\n  className?: string;\n};\n\nexport function CodeBlock({\n  code,\n  language,\n  filename,\n  showLineNumbers = true,\n  highlightLines,\n  maxHeight = 384,\n  onCopy,\n  className,\n}: CodeBlockProps) {\n  const reducedMotion = useReducedMotion() ?? false;\n  const [copied, setCopied] = useState(false);\n  const resetTimer = useRef<number | null>(null);\n\n  const lines = useMemo(() => {\n    const trimmed = code.replace(/^\\n+|\\s+$/g, \"\");\n    return trimmed.split(\"\\n\").map((line) => tokenizeLine(line));\n  }, [code]);\n\n  const highlighted = useMemo(\n    () => new Set(highlightLines ?? []),\n    [highlightLines]\n  );\n\n  const handleCopy = useCallback(async () => {\n    try {\n      await navigator.clipboard.writeText(code);\n    } catch {\n      // Clipboard API can be unavailable in iframes or without permission —\n      // fall back to a hidden textarea and the legacy copy command.\n      const textarea = document.createElement(\"textarea\");\n      textarea.value = code;\n      textarea.setAttribute(\"readonly\", \"\");\n      textarea.style.position = \"fixed\";\n      textarea.style.opacity = \"0\";\n      document.body.appendChild(textarea);\n      textarea.select();\n      const succeeded = document.execCommand(\"copy\");\n      textarea.remove();\n\n      if (!succeeded) {\n        return;\n      }\n    }\n\n    setCopied(true);\n    onCopy?.(code);\n\n    if (resetTimer.current !== null) {\n      window.clearTimeout(resetTimer.current);\n    }\n    resetTimer.current = window.setTimeout(() => {\n      setCopied(false);\n      resetTimer.current = null;\n    }, COPY_RESET_MS);\n  }, [code, onCopy]);\n\n  const hasTab = Boolean(filename);\n\n  return (\n    <div\n      className={cn(\n        \"overflow-hidden rounded-xl border border-border/80 bg-background shadow-[0_1px_2px_rgba(15,23,42,0.05),0_16px_40px_-20px_rgba(15,23,42,0.22)] dark:bg-zinc-950 dark:shadow-[0_1px_2px_rgba(0,0,0,0.4),0_16px_40px_-20px_rgba(0,0,0,0.55)]\",\n        className\n      )}\n      data-copied={copied || undefined}\n      data-slot=\"code-block\"\n    >\n      {hasTab ? (\n        // Editor-style tab strip: the active tab joins the code area below it,\n        // with the copy button pinned to the strip's right edge.\n        <div className=\"flex items-end justify-between gap-3 border-border/70 border-b bg-gradient-to-b from-muted/70 to-muted/40 pt-2 pr-1.5 pl-2.5 dark:from-white/[0.05] dark:to-white/[0.02]\">\n          <div className=\"relative -mb-px flex min-w-0 items-center gap-2 rounded-t-lg border border-border/70 border-b-0 bg-background px-3.5 py-1.5 dark:bg-zinc-950\">\n            <span className=\"truncate font-mono text-[12.5px] text-foreground/80\">\n              {filename}\n            </span>\n          </div>\n\n          <div className=\"mb-1 shrink-0\">\n            <CopyButton\n              copied={copied}\n              onCopy={handleCopy}\n              reducedMotion={reducedMotion}\n            />\n          </div>\n        </div>\n      ) : null}\n\n      <div className=\"relative\">\n        {hasTab ? null : (\n          // No tab strip — the copy button floats over the code, top right.\n          <div className=\"absolute top-2 right-2 z-10\">\n            <CopyButton\n              copied={copied}\n              floating\n              onCopy={handleCopy}\n              reducedMotion={reducedMotion}\n            />\n          </div>\n        )}\n\n        <div\n          className=\"overflow-auto [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2\"\n          style={{ maxHeight }}\n        >\n          <pre className=\"min-w-max py-3.5 font-mono text-[13px] leading-6 selection:bg-indigo-500/25\">\n            <code className=\"block\">\n              {lines.map((tokens, index) => {\n                const lineNumber = index + 1;\n                const isHighlighted = highlighted.has(lineNumber);\n\n                return (\n                  <span\n                    className={cn(\n                      \"relative block pr-5 transition-colors duration-100\",\n                      showLineNumbers ? \"pl-0\" : \"pl-4\",\n                      isHighlighted\n                        ? \"bg-gradient-to-r from-indigo-500/[0.12] via-indigo-500/[0.04] to-transparent dark:from-indigo-400/[0.16] dark:via-indigo-400/[0.05]\"\n                        : \"hover:bg-muted/60 dark:hover:bg-white/[0.04]\",\n                      isHighlighted &&\n                        !showLineNumbers &&\n                        \"shadow-[inset_2px_0_0_0_#6366f1]\"\n                    )}\n                    key={lineNumber}\n                  >\n                    {showLineNumbers ? (\n                      // Opaque gutter cell: long lines slide underneath it on\n                      // horizontal scroll instead of showing through the digits.\n                      <span\n                        aria-hidden\n                        className={cn(\n                          \"sticky left-0 z-10 mr-4 inline-block w-11 select-none border-border/60 border-r pr-3 text-right text-xs tabular-nums leading-6\",\n                          isHighlighted\n                            ? \"bg-[color-mix(in_srgb,#6366f1_10%,var(--color-background))] text-indigo-600 shadow-[inset_2px_0_0_0_#6366f1] dark:bg-[color-mix(in_srgb,#818cf8_14%,#09090b)] dark:text-indigo-400\"\n                            : \"bg-background text-muted-foreground/50 dark:bg-zinc-950\"\n                        )}\n                      >\n                        {lineNumber}\n                      </span>\n                    ) : null}\n                    {tokens.length === 0 ? (\n                      \"\\n\"\n                    ) : (\n                      <>\n                        {tokens.map((token, tokenIndex) => (\n                          <span\n                            className={TOKEN_CLASS_NAMES[token.kind]}\n                            key={`${lineNumber}-${token.kind}-${tokenIndex}`}\n                          >\n                            {token.value}\n                          </span>\n                        ))}\n                        {\"\\n\"}\n                      </>\n                    )}\n                  </span>\n                );\n              })}\n            </code>\n          </pre>\n        </div>\n      </div>\n\n      {/* Status bar: language and line count, plus a copy confirmation. */}\n      <div className=\"flex items-center justify-between gap-3 border-border/70 border-t bg-muted/60 px-4 py-1.5 text-[11px] text-muted-foreground dark:bg-white/[0.04]\">\n        <div className=\"flex min-w-0 items-center gap-2\">\n          {language ? (\n            <>\n              <span className=\"truncate font-mono uppercase tracking-wider\">\n                {language}\n              </span>\n              <span aria-hidden className=\"text-muted-foreground/50\">\n                ·\n              </span>\n            </>\n          ) : null}\n          <span className=\"shrink-0 tabular-nums\">\n            {lines.length} {lines.length === 1 ? \"line\" : \"lines\"}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "title": "Code Block",
  "description": "Editor-style code block with a filename tab, a top-right spring-crossfade copy button, a bottom status bar with language and line count, line numbers, built-in theme-aware syntax highlighting, and line emphasis."
}
