{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-code-block",
  "title": "Chat Code Block",
  "description": "Code highlighted in the light and dark themes, with a copy button that turns into a check.",
  "dependencies": [
    "shiki"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/chat-motion.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/ui/chat-code-block.tsx",
      "content": "// Adapted from prompt-kit (MIT, Copyright (c) 2025 Julien Thibeaut).\n'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { bundledLanguages, codeToHtml, type BundledTheme } from 'shiki';\n\nimport { CheckIcon, CopyIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Swap } from '@/registry/ndeko/ui/chat-motion';\n\ntype CodeThemes = {\n  light: BundledTheme;\n  dark: BundledTheme;\n};\n\nconst DEFAULT_THEMES: CodeThemes = {\n  light: 'github-light',\n  dark: 'github-dark',\n};\n\nconst PLAIN_LANGUAGE = 'text';\n\nconst COPIED_FOR_MS = 1600;\n\ntype CodeBlockProps = React.ComponentProps<'div'>;\n\nfunction CodeBlock({ className, ...props }: CodeBlockProps) {\n  return (\n    <div\n      data-slot='code-block'\n      className={cn(\n        'not-prose bg-card text-card-foreground hairline-ring ring-border flex w-full flex-col overflow-clip rounded-xl shadow-[0_1px_2px_rgb(0_0_0/0.04)]',\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction isBundledLanguage(language: string) {\n  return language in bundledLanguages;\n}\n\nfunction highlightableLanguageOf(language: string) {\n  if (isBundledLanguage(language)) {\n    return language;\n  }\n  return PLAIN_LANGUAGE;\n}\n\nfunction highlightCode(code: string, language: string, themes: CodeThemes) {\n  const lang = highlightableLanguageOf(language);\n  return codeToHtml(code, { lang, themes, defaultColor: false });\n}\n\nfunction keepPlainCode() {\n  return null;\n}\n\n/** Shiki writes both themes as CSS variables on every token; the `dark` class picks which one paints. */\nconst CODE_AREA = [\n  'w-full overflow-x-auto font-mono text-[13px] leading-relaxed',\n  '[&_pre]:min-w-max [&_pre]:bg-transparent [&_pre]:px-4 [&_pre]:py-3 [&_pre]:outline-none',\n  '[&_.shiki]:text-(--shiki-light) [&_.shiki_span]:text-(--shiki-light)',\n  'dark:[&_.shiki]:text-(--shiki-dark) dark:[&_.shiki_span]:text-(--shiki-dark)',\n].join(' ');\n\ntype CodeBlockCodeProps = Omit<\n  React.ComponentProps<'div'>,\n  'children' | 'dangerouslySetInnerHTML'\n> & {\n  code: string;\n  language?: string;\n  themes?: CodeThemes;\n};\n\nfunction CodeBlockCode({\n  code,\n  language = 'tsx',\n  themes = DEFAULT_THEMES,\n  className,\n  ...props\n}: CodeBlockCodeProps) {\n  const [highlightedHtml, setHighlightedHtml] = useState<string | null>(null);\n  const lightTheme = themes.light;\n  const darkTheme = themes.dark;\n  const areaClassName = cn(CODE_AREA, className);\n\n  useEffect(() => {\n    let isCurrent = true;\n    const pair = { light: lightTheme, dark: darkTheme };\n\n    function applyHighlight(html: string) {\n      if (isCurrent) {\n        setHighlightedHtml(html);\n      }\n    }\n\n    highlightCode(code, language, pair)\n      .then(applyHighlight)\n      .catch(keepPlainCode);\n\n    return () => {\n      isCurrent = false;\n    };\n  }, [code, language, lightTheme, darkTheme]);\n\n  if (!highlightedHtml) {\n    return (\n      <div data-slot='code-block-code' className={areaClassName} {...props}>\n        <pre>\n          <code>{code}</code>\n        </pre>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      data-slot='code-block-code'\n      className={areaClassName}\n      dangerouslySetInnerHTML={{ __html: highlightedHtml }}\n      {...props}\n    />\n  );\n}\n\ntype CodeBlockGroupProps = React.ComponentProps<'div'>;\n\nfunction CodeBlockGroup({ className, ...props }: CodeBlockGroupProps) {\n  return (\n    <div\n      data-slot='code-block-group'\n      className={cn(\n        'flex items-center justify-between gap-2 py-1.5 ps-4 pe-1.5',\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction CodeBlockLanguage({\n  className,\n  ...props\n}: React.ComponentProps<'span'>) {\n  return (\n    <span\n      data-slot='code-block-language'\n      className={cn('text-muted-foreground font-mono text-xs', className)}\n      {...props}\n    />\n  );\n}\n\nfunction useCopiedFlag() {\n  const [isCopied, setIsCopied] = useState(false);\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(() => {\n    return () => {\n      if (timer.current) {\n        clearTimeout(timer.current);\n      }\n    };\n  }, []);\n\n  function flagCopied() {\n    if (timer.current) {\n      clearTimeout(timer.current);\n    }\n    setIsCopied(true);\n    timer.current = setTimeout(() => setIsCopied(false), COPIED_FOR_MS);\n  }\n\n  return { isCopied, flagCopied };\n}\n\nfunction copyLabelOf(isCopied: boolean) {\n  if (isCopied) {\n    return 'Copied';\n  }\n  return 'Copy code';\n}\n\ntype CodeBlockCopyButtonProps = Omit<\n  React.ComponentProps<typeof Button>,\n  'children' | 'onClick'\n> & {\n  code: string;\n  onCopyError?: (error: unknown) => void;\n};\n\nfunction CodeBlockCopyButton({\n  code,\n  onCopyError,\n  className,\n  ...props\n}: CodeBlockCopyButtonProps) {\n  const { isCopied, flagCopied } = useCopiedFlag();\n  const label = copyLabelOf(isCopied);\n  const iconKey = String(isCopied);\n\n  async function copy() {\n    try {\n      await navigator.clipboard.writeText(code);\n    } catch (error) {\n      onCopyError?.(error);\n      return;\n    }\n    flagCopied();\n  }\n\n  return (\n    <Button\n      type='button'\n      variant='ghost'\n      size='icon-sm'\n      aria-label={label}\n      onClick={copy}\n      className={cn('text-muted-foreground', className)}\n      {...props}\n    >\n      <Swap swapKey={iconKey}>\n        {isCopied && <CheckIcon className='size-3.5' />}\n        {!isCopied && <CopyIcon className='size-3.5' />}\n      </Swap>\n    </Button>\n  );\n}\n\nexport {\n  CodeBlock,\n  CodeBlockCode,\n  CodeBlockCopyButton,\n  CodeBlockGroup,\n  CodeBlockLanguage,\n  type CodeThemes,\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "primitives",
    "chat"
  ],
  "type": "registry:ui"
}