{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "title": "Code Block",
  "description": "A source-code display with language and filename metadata, copying, and optional line numbers.",
  "dependencies": [
    "lucide-react",
    "prism-react-renderer"
  ],
  "files": [
    {
      "path": "registry/rivetui/components/code-block/code-block.tsx",
      "content": "\"use client\";\n\nimport { Check, Copy } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { SyntaxHighlight } from \"@/lib/syntax-highlight\";\nimport { cn } from \"@/lib/utils\";\n\ntype CodeBlockProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  code: string;\n  language?: string;\n  filename?: string;\n  copyable?: boolean;\n  showLineNumbers?: boolean;\n  highlightLinesOnHover?: boolean;\n  softWrap?: boolean;\n};\n\nfunction CodeBlock({\n  code,\n  language,\n  filename,\n  copyable = true,\n  showLineNumbers = false,\n  highlightLinesOnHover = false,\n  softWrap = false,\n  className,\n  ...props\n}: CodeBlockProps) {\n  const [copied, setCopied] = React.useState(false);\n  const hasHeader = filename !== undefined || language !== undefined || copyable;\n\n  async function copyCode() {\n    try {\n      await navigator.clipboard.writeText(code);\n      setCopied(true);\n      window.setTimeout(() => setCopied(false), 1600);\n    } catch {\n      setCopied(false);\n    }\n  }\n\n  return (\n    <div\n      data-slot=\"code-block\"\n      data-language={language}\n      data-soft-wrap={softWrap ? \"true\" : undefined}\n      className={cn(\"min-w-0 overflow-hidden border border-border bg-card\", className)}\n      {...props}\n    >\n      {hasHeader ? (\n        <div\n          data-slot=\"code-block-header\"\n          className=\"flex min-h-10 items-stretch border-b border-border bg-muted/20\"\n        >\n          <div className=\"flex min-w-0 flex-1 items-center gap-3 px-3 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-muted-foreground\">\n            {filename ? <span className=\"truncate text-foreground\">{filename}</span> : null}\n            {language ? <span className=\"ml-auto shrink-0\">{language}</span> : null}\n          </div>\n          {copyable ? (\n            <button\n              type=\"button\"\n              data-slot=\"code-block-copy\"\n              onClick={copyCode}\n              className=\"inline-flex size-10 shrink-0 cursor-pointer items-center justify-center border-l border-border text-muted-foreground outline-none transition-colors duration-100 hover:bg-muted/60 hover:text-foreground focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 motion-reduce:transition-none\"\n              aria-label={copied ? \"Code copied\" : \"Copy code\"}\n            >\n              {copied ? (\n                <Check className=\"size-3.5\" aria-hidden=\"true\" />\n              ) : (\n                <Copy className=\"size-3.5\" aria-hidden=\"true\" />\n              )}\n              <span className=\"sr-only\" aria-live=\"polite\">\n                {copied ? \"Code copied to clipboard\" : \"\"}\n              </span>\n            </button>\n          ) : null}\n        </div>\n      ) : null}\n\n      <pre\n        data-slot=\"code-block-content\"\n        className={cn(\n          \"min-w-0 py-4 font-mono text-xs leading-6\",\n          softWrap ? \"overflow-x-hidden\" : \"overflow-x-auto\",\n          !showLineNumbers && !highlightLinesOnHover && \"px-4\",\n        )}\n      >\n        <SyntaxHighlight\n          code={code}\n          language={language}\n          showLineNumbers={showLineNumbers}\n          highlightLinesOnHover={highlightLinesOnHover}\n          softWrap={softWrap}\n        />\n      </pre>\n    </div>\n  );\n}\n\nexport type { CodeBlockProps };\nexport { CodeBlock };\n",
      "type": "registry:ui"
    },
    {
      "path": "lib/syntax-highlight.tsx",
      "content": "import {\n  Highlight,\n  normalizeTokens,\n  Prism,\n  type PrismGrammar,\n  type PrismTheme,\n  type Token as SyntaxToken,\n} from \"prism-react-renderer\";\n\nconst languageAliases: Record<string, string> = {\n  html: \"markup\",\n  js: \"javascript\",\n  jsx: \"jsx\",\n  md: \"markdown\",\n  sh: \"bash\",\n  shell: \"bash\",\n  ts: \"typescript\",\n  tsx: \"tsx\",\n  xml: \"markup\",\n  yml: \"yaml\",\n};\n\nconst shellGrammar: PrismGrammar = {\n  comment: {\n    pattern: /(^|[^\\\\])#.*/,\n    lookbehind: true,\n    greedy: true,\n  },\n  string: [\n    {\n      pattern: /\"(?:\\\\[\\s\\S]|[^\"\\\\])*\"/,\n      greedy: true,\n    },\n    {\n      pattern: /'(?:[^']*)'/,\n      greedy: true,\n    },\n  ],\n  variable: [\n    /\\$(?:[a-z_][\\w]*|\\d+|[?$!#@*-])/i,\n    {\n      pattern: /\\$\\{[^}]+\\}/,\n      greedy: true,\n    },\n  ],\n  option: {\n    pattern: /(^|\\s)--?[\\w-]+(?:=[^\\s]+)?/,\n    lookbehind: true,\n    alias: \"attr-name\",\n  },\n  command: {\n    pattern: /(^|[\\n;|&]\\s*)[\\w@./-]+/m,\n    lookbehind: true,\n    alias: \"function\",\n  },\n  symbol: /@[\\w./-]+/,\n  operator: /&&|\\|\\||[|;&]|>>?|<<?/,\n  punctuation: /[()[\\]{}]/,\n};\n\nif (!Prism.languages.bash) {\n  Prism.languages.bash = shellGrammar;\n}\n\nconst rivetSyntaxThemeClassName = [\n  \"[--syntax-comment:#6f7378]\",\n  \"[--syntax-foreground:#24292e]\",\n  \"[--syntax-function:#6f42c1]\",\n  \"[--syntax-keyword:#d32f2f]\",\n  \"[--syntax-number:#1976d2]\",\n  \"[--syntax-punctuation:#212121]\",\n  \"[--syntax-string:#22863a]\",\n  \"[--syntax-text:#24292e]\",\n  \"[--syntax-variable:#1976d2]\",\n  \"dark:[--syntax-comment:#8b949e]\",\n  \"dark:[--syntax-foreground:#b392f0]\",\n  \"dark:[--syntax-function:#b392f0]\",\n  \"dark:[--syntax-keyword:#f97583]\",\n  \"dark:[--syntax-number:#f8f8f8]\",\n  \"dark:[--syntax-punctuation:#bbbbbb]\",\n  \"dark:[--syntax-string:#ffab70]\",\n  \"dark:[--syntax-text:#f8f8f8]\",\n  \"dark:[--syntax-variable:#79b8ff]\",\n].join(\" \");\n\nconst rivetSyntaxTheme: PrismTheme = {\n  plain: {\n    color: \"var(--syntax-foreground)\",\n  },\n  styles: [\n    {\n      types: [\"comment\", \"prolog\", \"doctype\", \"cdata\"],\n      style: {\n        color: \"var(--syntax-comment)\",\n        fontStyle: \"italic\",\n      },\n    },\n    {\n      types: [\"punctuation\", \"operator\", \"entity\", \"url\"],\n      style: {\n        color: \"var(--syntax-punctuation)\",\n      },\n    },\n    {\n      types: [\"string\", \"char\", \"attr-value\", \"regex\", \"inserted\"],\n      style: {\n        color: \"var(--syntax-string)\",\n      },\n    },\n    {\n      types: [\"keyword\", \"module\", \"control-flow\", \"important\", \"atrule\"],\n      style: {\n        color: \"var(--syntax-keyword)\",\n      },\n    },\n    {\n      types: [\"function\", \"function-variable\", \"class-name\", \"builtin\", \"property\", \"tag\"],\n      style: {\n        color: \"var(--syntax-function)\",\n      },\n    },\n    {\n      types: [\"attr-name\", \"variable\"],\n      style: {\n        color: \"var(--syntax-variable)\",\n      },\n    },\n    {\n      types: [\"boolean\", \"number\", \"constant\", \"symbol\"],\n      style: {\n        color: \"var(--syntax-number)\",\n      },\n    },\n    {\n      types: [\"deleted\"],\n      style: {\n        color: \"var(--destructive)\",\n      },\n    },\n  ],\n};\n\ntype SyntaxHighlightProps = {\n  code: string;\n  language?: string;\n  showLineNumbers?: boolean;\n  highlightLinesOnHover?: boolean;\n  softWrap?: boolean;\n};\n\nfunction normalizeLanguage(language: string | undefined) {\n  const normalizedLanguage = language?.trim().toLowerCase() || \"text\";\n\n  return languageAliases[normalizedLanguage] ?? normalizedLanguage;\n}\n\nfunction getLineIds(code: string) {\n  const occurrences = new Map<string, number>();\n\n  return code.split(\"\\n\").map((content) => {\n    const occurrence = (occurrences.get(content) ?? 0) + 1;\n    occurrences.set(content, occurrence);\n\n    return `${content}-${occurrence}`;\n  });\n}\n\nfunction findJsxTagEnd(content: string) {\n  let braceDepth = 0;\n  let quote: '\"' | \"'\" | null = null;\n  let isEscaped = false;\n\n  for (let index = 1; index < content.length; index += 1) {\n    const character = content[index];\n\n    if (quote) {\n      if (isEscaped) {\n        isEscaped = false;\n      } else if (character === \"\\\\\") {\n        isEscaped = true;\n      } else if (character === quote) {\n        quote = null;\n      }\n\n      continue;\n    }\n\n    if (character === '\"' || character === \"'\") {\n      quote = character;\n    } else if (character === \"{\") {\n      braceDepth += 1;\n    } else if (character === \"}\") {\n      braceDepth = Math.max(0, braceDepth - 1);\n    } else if (character === \">\" && braceDepth === 0) {\n      return index;\n    }\n  }\n\n  return -1;\n}\n\nfunction normalizeEmbeddedJsxTags(tokens: SyntaxToken[][], language: string) {\n  if (language !== \"jsx\" && language !== \"tsx\") {\n    return tokens;\n  }\n\n  return tokens.map((line) =>\n    line.flatMap((token) => {\n      if (!token.types.includes(\"plain-text\") || !/^<[A-Za-z]/.test(token.content)) {\n        return token;\n      }\n\n      const normalized: SyntaxToken[] = [];\n      let remainingContent = token.content;\n\n      while (/^<[A-Za-z]/.test(remainingContent)) {\n        const tagEnd = findJsxTagEnd(remainingContent);\n\n        if (tagEnd === -1) {\n          break;\n        }\n\n        const tag = remainingContent.slice(0, tagEnd + 1);\n        const tagTokens = normalizeTokens(Prism.tokenize(tag, Prism.languages.jsx)).flat();\n        normalized.push(...tagTokens);\n        remainingContent = remainingContent.slice(tagEnd + 1);\n      }\n\n      if (remainingContent) {\n        normalized.push({ ...token, content: remainingContent });\n      }\n\n      return normalized.length > 0 ? normalized : token;\n    }),\n  );\n}\n\nfunction getJsxTextTokenKeys(tokens: SyntaxToken[][], language: string) {\n  const textTokenKeys = new Set<string>();\n\n  if (language !== \"jsx\" && language !== \"tsx\") {\n    return textTokenKeys;\n  }\n\n  let jsxDepth = 0;\n  let expressionDepth = 0;\n  let isInsideTag = false;\n  let isClosingTag = false;\n\n  tokens.forEach((line, lineIndex) => {\n    line.forEach((token, tokenIndex) => {\n      const { content, types } = token;\n\n      if (isInsideTag) {\n        if (content.includes(\"/>\")) {\n          isInsideTag = false;\n          isClosingTag = false;\n          return;\n        }\n\n        if (content.includes(\">\")) {\n          jsxDepth = isClosingTag ? Math.max(0, jsxDepth - 1) : jsxDepth + 1;\n          isInsideTag = false;\n          isClosingTag = false;\n        }\n\n        return;\n      }\n\n      if (expressionDepth > 0) {\n        expressionDepth += (content.match(/\\{/g) ?? []).length;\n        expressionDepth -= (content.match(/\\}/g) ?? []).length;\n        return;\n      }\n\n      if (jsxDepth > 0 && content.includes(\"{\")) {\n        expressionDepth = (content.match(/\\{/g) ?? []).length - (content.match(/\\}/g) ?? []).length;\n        return;\n      }\n\n      const startsTag =\n        (content === \"<\" || content === \"</\") && (types.includes(\"tag\") || jsxDepth > 0);\n\n      if (startsTag) {\n        isInsideTag = true;\n        isClosingTag = content === \"</\";\n        return;\n      }\n\n      if (jsxDepth > 0 && content.trim()) {\n        textTokenKeys.add(`${lineIndex}:${tokenIndex}`);\n      }\n    });\n  });\n\n  return textTokenKeys;\n}\n\nfunction SyntaxHighlight({\n  code,\n  language,\n  showLineNumbers = false,\n  highlightLinesOnHover = false,\n  softWrap = false,\n}: SyntaxHighlightProps) {\n  const normalizedLanguage = normalizeLanguage(language);\n  const lineIds = getLineIds(code);\n  const lineNumberWidth = `calc(${String(lineIds.length).length}ch + 1.5rem)`;\n\n  return (\n    <Highlight code={code} language={normalizedLanguage} theme={rivetSyntaxTheme}>\n      {({ tokens, getLineProps, getTokenProps }) => {\n        const normalizedTokens = normalizeEmbeddedJsxTags(tokens, normalizedLanguage);\n        const jsxTextTokenKeys = getJsxTextTokenKeys(normalizedTokens, normalizedLanguage);\n\n        return (\n          <code\n            data-language={language}\n            className={`language-${normalizedLanguage} ${rivetSyntaxThemeClassName} ${\n              softWrap ? \"block min-w-0 whitespace-pre-wrap wrap-anywhere\" : \"\"\n            }`}\n          >\n            {normalizedTokens.map((line, lineIndex) => {\n              const lineProps = getLineProps({ line });\n              const tokenOccurrences = new Map<string, number>();\n              const content = line.map((token, tokenIndex) => {\n                const tokenProps = getTokenProps({ token });\n                const signature = `${token.types.join(\":\")}-${token.content}`;\n                const occurrence = (tokenOccurrences.get(signature) ?? 0) + 1;\n                tokenOccurrences.set(signature, occurrence);\n\n                if (jsxTextTokenKeys.has(`${lineIndex}:${tokenIndex}`)) {\n                  tokenProps.style = {\n                    ...tokenProps.style,\n                    color: \"var(--syntax-text)\",\n                  };\n                }\n\n                return <span {...tokenProps} key={`${signature}-${occurrence}`} />;\n              });\n\n              if (showLineNumbers || highlightLinesOnHover) {\n                return (\n                  <span\n                    {...lineProps}\n                    key={lineIds[lineIndex]}\n                    className={`${lineProps.className} flex w-full ${\n                      softWrap ? \"min-w-0\" : \"min-w-max\"\n                    } ${\n                      highlightLinesOnHover\n                        ? \"transition-colors duration-100 hover:bg-muted/50 motion-reduce:transition-none\"\n                        : \"\"\n                    }`}\n                  >\n                    {showLineNumbers ? (\n                      <span\n                        key=\"line-number\"\n                        aria-hidden=\"true\"\n                        className=\"shrink-0 select-none border-r border-border pr-2 pl-4 text-right tabular-nums text-muted-foreground/60\"\n                        style={{ width: lineNumberWidth }}\n                      >\n                        {lineIndex + 1}\n                      </span>\n                    ) : null}\n                    <span\n                      key=\"line-content\"\n                      className={`px-4 ${\n                        softWrap\n                          ? \"min-w-0 flex-1 whitespace-pre-wrap wrap-anywhere\"\n                          : \"min-w-max flex-1 whitespace-pre\"\n                      }`}\n                    >\n                      {content}\n                    </span>\n                  </span>\n                );\n              }\n\n              return (\n                <span {...lineProps} key={lineIds[lineIndex]}>\n                  {content}\n                  {lineIndex < normalizedTokens.length - 1 ? \"\\n\" : null}\n                </span>\n              );\n            })}\n          </code>\n        );\n      }}\n    </Highlight>\n  );\n}\n\nexport type { SyntaxHighlightProps };\nexport { SyntaxHighlight };\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}