{"$schema":"https://ui.shadcn.com/schema/registry-item.json","dependencies":["lucide-react","shiki"],"description":"AI-powered code block component.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/default/ui/select\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, HTMLAttributes } from \"react\";\nimport {\n  createContext,\n  memo,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport type {\n  BundledLanguage,\n  BundledTheme,\n  HighlighterGeneric,\n  ThemedToken,\n} from \"shiki\";\nimport { createHighlighter } from \"shiki\";\n\n// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline\n// oxlint-disable-next-line eslint(no-bitwise)\nconst isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;\n// oxlint-disable-next-line eslint(no-bitwise)\nconst isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;\nconst isUnderline = (fontStyle: number | undefined) =>\n  // oxlint-disable-next-line eslint(no-bitwise)\n  fontStyle && fontStyle & 4;\n\n// Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint\ninterface KeyedToken {\n  token: ThemedToken;\n  key: string;\n}\ninterface KeyedLine {\n  tokens: KeyedToken[];\n  key: string;\n}\n\nconst addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>\n  lines.map((line, lineIdx) => ({\n    key: `line-${lineIdx}`,\n    tokens: line.map((token, tokenIdx) => ({\n      key: `line-${lineIdx}-${tokenIdx}`,\n      token,\n    })),\n  }));\n\n// Token rendering component\nconst TokenSpan = ({ token }: { token: ThemedToken }) => (\n  <span\n    className=\"dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]\"\n    style={\n      {\n        backgroundColor: token.bgColor,\n        color: token.color,\n        fontStyle: isItalic(token.fontStyle) ? \"italic\" : undefined,\n        fontWeight: isBold(token.fontStyle) ? \"bold\" : undefined,\n        textDecoration: isUnderline(token.fontStyle) ? \"underline\" : undefined,\n        ...token.htmlStyle,\n      } as CSSProperties\n    }\n  >\n    {token.content}\n  </span>\n);\n\n// Line number styles using CSS counters\nconst LINE_NUMBER_CLASSES = cn(\n  \"block\",\n  \"before:content-[counter(line)]\",\n  \"before:inline-block\",\n  \"before:[counter-increment:line]\",\n  \"before:w-8\",\n  \"before:mr-4\",\n  \"before:text-right\",\n  \"before:text-muted-foreground/50\",\n  \"before:font-mono\",\n  \"before:select-none\"\n);\n\n// Line rendering component\nconst LineSpan = ({\n  keyedLine,\n  showLineNumbers,\n}: {\n  keyedLine: KeyedLine;\n  showLineNumbers: boolean;\n}) => (\n  <span className={showLineNumbers ? LINE_NUMBER_CLASSES : \"block\"}>\n    {keyedLine.tokens.length === 0\n      ? \"\\n\"\n      : keyedLine.tokens.map(({ token, key }) => (\n          <TokenSpan key={key} token={token} />\n        ))}\n  </span>\n);\n\n// Types\ntype CodeBlockProps = HTMLAttributes<HTMLDivElement> & {\n  code: string;\n  language: BundledLanguage;\n  showLineNumbers?: boolean;\n};\n\ninterface TokenizedCode {\n  tokens: ThemedToken[][];\n  fg: string;\n  bg: string;\n}\n\ninterface CodeBlockContextType {\n  code: string;\n}\n\n// Context\nconst CodeBlockContext = createContext<CodeBlockContextType>({\n  code: \"\",\n});\n\n// Highlighter cache (singleton per language)\nconst highlighterCache = new Map<\n  string,\n  Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>\n>();\n\n// Token cache\nconst tokensCache = new Map<string, TokenizedCode>();\n\n// Subscribers for async token updates\nconst subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();\n\nconst getTokensCacheKey = (code: string, language: BundledLanguage) => {\n  const start = code.slice(0, 100);\n  const end = code.length > 100 ? code.slice(-100) : \"\";\n  return `${language}:${code.length}:${start}:${end}`;\n};\n\nconst getHighlighter = (\n  language: BundledLanguage\n): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {\n  const cached = highlighterCache.get(language);\n  if (cached) {\n    return cached;\n  }\n\n  const highlighterPromise = createHighlighter({\n    langs: [language],\n    themes: [\"github-light\", \"github-dark\"],\n  });\n\n  highlighterCache.set(language, highlighterPromise);\n  return highlighterPromise;\n};\n\n// Create raw tokens for immediate display while highlighting loads\nconst createRawTokens = (code: string): TokenizedCode => ({\n  bg: \"transparent\",\n  fg: \"inherit\",\n  tokens: code.split(\"\\n\").map((line) =>\n    line === \"\"\n      ? []\n      : [\n          {\n            color: \"inherit\",\n            content: line,\n          } as ThemedToken,\n        ]\n  ),\n});\n\n// Synchronous highlight with callback for async results\nexport const highlightCode = (\n  code: string,\n  language: BundledLanguage,\n  // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)\n  callback?: (result: TokenizedCode) => void\n): TokenizedCode | null => {\n  const tokensCacheKey = getTokensCacheKey(code, language);\n\n  // Return cached result if available\n  const cached = tokensCache.get(tokensCacheKey);\n  if (cached) {\n    return cached;\n  }\n\n  // Subscribe callback if provided\n  if (callback) {\n    if (!subscribers.has(tokensCacheKey)) {\n      subscribers.set(tokensCacheKey, new Set());\n    }\n    subscribers.get(tokensCacheKey)?.add(callback);\n  }\n\n  // Start highlighting in background - fire-and-forget async pattern\n  getHighlighter(language)\n    // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)\n    .then((highlighter) => {\n      const availableLangs = highlighter.getLoadedLanguages();\n      const langToUse = availableLangs.includes(language) ? language : \"text\";\n\n      const result = highlighter.codeToTokens(code, {\n        lang: langToUse,\n        themes: {\n          dark: \"github-dark\",\n          light: \"github-light\",\n        },\n      });\n\n      const tokenized: TokenizedCode = {\n        bg: result.bg ?? \"transparent\",\n        fg: result.fg ?? \"inherit\",\n        tokens: result.tokens,\n      };\n\n      // Cache the result\n      tokensCache.set(tokensCacheKey, tokenized);\n\n      // Notify all subscribers\n      const subs = subscribers.get(tokensCacheKey);\n      if (subs) {\n        for (const sub of subs) {\n          sub(tokenized);\n        }\n        subscribers.delete(tokensCacheKey);\n      }\n    })\n    // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)\n    .catch((error) => {\n      console.error(\"Failed to highlight code:\", error);\n      subscribers.delete(tokensCacheKey);\n    });\n\n  return null;\n};\n\nconst CodeBlockBody = memo(\n  ({\n    tokenized,\n    showLineNumbers,\n    className,\n  }: {\n    tokenized: TokenizedCode;\n    showLineNumbers: boolean;\n    className?: string;\n  }) => {\n    const preStyle = useMemo(\n      () => ({\n        backgroundColor: tokenized.bg,\n        color: tokenized.fg,\n      }),\n      [tokenized.bg, tokenized.fg]\n    );\n\n    const keyedLines = useMemo(\n      () => addKeysToTokens(tokenized.tokens),\n      [tokenized.tokens]\n    );\n\n    return (\n      <pre\n        className={cn(\n          \"dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm\",\n          className\n        )}\n        style={preStyle}\n      >\n        <code\n          className={cn(\n            \"font-mono text-sm\",\n            showLineNumbers && \"[counter-increment:line_0] [counter-reset:line]\"\n          )}\n        >\n          {keyedLines.map((keyedLine) => (\n            <LineSpan\n              key={keyedLine.key}\n              keyedLine={keyedLine}\n              showLineNumbers={showLineNumbers}\n            />\n          ))}\n        </code>\n      </pre>\n    );\n  },\n  (prevProps, nextProps) =>\n    prevProps.tokenized === nextProps.tokenized &&\n    prevProps.showLineNumbers === nextProps.showLineNumbers &&\n    prevProps.className === nextProps.className\n);\n\nCodeBlockBody.displayName = \"CodeBlockBody\";\n\nexport const CodeBlockContainer = ({\n  className,\n  language,\n  style,\n  ...props\n}: HTMLAttributes<HTMLDivElement> & { language: string }) => (\n  <div\n    className={cn(\n      \"group relative w-full overflow-hidden rounded-md border bg-background text-foreground\",\n      className\n    )}\n    data-language={language}\n    style={{\n      containIntrinsicSize: \"auto 200px\",\n      contentVisibility: \"auto\",\n      ...style,\n    }}\n    {...props}\n  />\n);\n\nexport const CodeBlockHeader = ({\n  children,\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n  </div>\n);\n\nexport const CodeBlockTitle = ({\n  children,\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"flex items-center gap-2\", className)} {...props}>\n    {children}\n  </div>\n);\n\nexport const CodeBlockFilename = ({\n  children,\n  className,\n  ...props\n}: HTMLAttributes<HTMLSpanElement>) => (\n  <span className={cn(\"font-mono\", className)} {...props}>\n    {children}\n  </span>\n);\n\nexport const CodeBlockActions = ({\n  children,\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"-my-1 -mr-1 flex items-center gap-2\", className)}\n    {...props}\n  >\n    {children}\n  </div>\n);\n\nexport const CodeBlockContent = ({\n  code,\n  language,\n  showLineNumbers = false,\n}: {\n  code: string;\n  language: BundledLanguage;\n  showLineNumbers?: boolean;\n}) => {\n  // Memoized raw tokens for immediate display\n  const rawTokens = useMemo(() => createRawTokens(code), [code]);\n\n  // Synchronous cache lookup — avoids setState in effect for cached results\n  const syncTokens = useMemo(\n    () => highlightCode(code, language) ?? rawTokens,\n    [code, language, rawTokens]\n  );\n\n  // Async highlighting result (populated after shiki loads)\n  const [asyncTokens, setAsyncTokens] = useState<TokenizedCode | null>(null);\n  const asyncKeyRef = useRef({ code, language });\n\n  // Invalidate stale async tokens synchronously during render\n  if (\n    asyncKeyRef.current.code !== code ||\n    asyncKeyRef.current.language !== language\n  ) {\n    asyncKeyRef.current = { code, language };\n    setAsyncTokens(null);\n  }\n\n  useEffect(() => {\n    let cancelled = false;\n\n    highlightCode(code, language, (result) => {\n      if (!cancelled) {\n        setAsyncTokens(result);\n      }\n    });\n\n    return () => {\n      cancelled = true;\n    };\n  }, [code, language]);\n\n  const tokenized = asyncTokens ?? syncTokens;\n\n  return (\n    <div className=\"relative overflow-auto\">\n      <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />\n    </div>\n  );\n};\n\nexport const CodeBlock = ({\n  code,\n  language,\n  showLineNumbers = false,\n  className,\n  children,\n  ...props\n}: CodeBlockProps) => {\n  const contextValue = useMemo(() => ({ code }), [code]);\n\n  return (\n    <CodeBlockContext.Provider value={contextValue}>\n      <CodeBlockContainer className={className} language={language} {...props}>\n        {children}\n        <CodeBlockContent\n          code={code}\n          language={language}\n          showLineNumbers={showLineNumbers}\n        />\n      </CodeBlockContainer>\n    </CodeBlockContext.Provider>\n  );\n};\n\nexport type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {\n  onCopy?: () => void;\n  onError?: (error: Error) => void;\n  timeout?: number;\n};\n\nexport const CodeBlockCopyButton = ({\n  onCopy,\n  onError,\n  timeout = 2000,\n  children,\n  className,\n  ...props\n}: CodeBlockCopyButtonProps) => {\n  const [isCopied, setIsCopied] = useState(false);\n  const timeoutRef = useRef<number>(0);\n  const { code } = useContext(CodeBlockContext);\n\n  const copyToClipboard = useCallback(async () => {\n    if (typeof window === \"undefined\" || !navigator?.clipboard?.writeText) {\n      onError?.(new Error(\"Clipboard API not available\"));\n      return;\n    }\n\n    try {\n      if (!isCopied) {\n        await navigator.clipboard.writeText(code);\n        setIsCopied(true);\n        onCopy?.();\n        timeoutRef.current = window.setTimeout(\n          () => setIsCopied(false),\n          timeout\n        );\n      }\n    } catch (error) {\n      onError?.(error as Error);\n    }\n  }, [code, onCopy, onError, timeout, isCopied]);\n\n  useEffect(\n    () => () => {\n      window.clearTimeout(timeoutRef.current);\n    },\n    []\n  );\n\n  const Icon = isCopied ? CheckIcon : CopyIcon;\n\n  return (\n    <Button\n      className={cn(\"shrink-0\", className)}\n      onClick={copyToClipboard}\n      size=\"icon\"\n      variant=\"ghost\"\n      {...props}\n    >\n      {children ?? <Icon size={14} />}\n    </Button>\n  );\n};\n\nexport type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;\n\nexport const CodeBlockLanguageSelector = (\n  props: CodeBlockLanguageSelectorProps\n) => <Select {...props} />;\n\nexport type CodeBlockLanguageSelectorTriggerProps = ComponentProps<\n  typeof SelectTrigger\n>;\n\nexport const CodeBlockLanguageSelectorTrigger = ({\n  className,\n  ...props\n}: CodeBlockLanguageSelectorTriggerProps) => (\n  <SelectTrigger\n    className={cn(\n      \"h-7 border-none bg-transparent px-2 text-xs shadow-none\",\n      className\n    )}\n    size=\"sm\"\n    {...props}\n  />\n);\n\nexport type CodeBlockLanguageSelectorValueProps = ComponentProps<\n  typeof SelectValue\n>;\n\nexport const CodeBlockLanguageSelectorValue = (\n  props: CodeBlockLanguageSelectorValueProps\n) => <SelectValue {...props} />;\n\nexport type CodeBlockLanguageSelectorContentProps = ComponentProps<\n  typeof SelectContent\n>;\n\nexport const CodeBlockLanguageSelectorContent = ({\n  align = \"end\",\n  ...props\n}: CodeBlockLanguageSelectorContentProps) => (\n  <SelectContent align={align} {...props} />\n);\n\nexport type CodeBlockLanguageSelectorItemProps = ComponentProps<\n  typeof SelectItem\n>;\n\nexport const CodeBlockLanguageSelectorItem = (\n  props: CodeBlockLanguageSelectorItemProps\n) => <SelectItem {...props} />;\n","path":"registry/default/ai-elements/code-block.tsx","target":"components/ai-elements/code-block.tsx","type":"registry:component"}],"name":"code-block","registryDependencies":["button","select"],"title":"Code Block","type":"registry:component"}