{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "code-block",
  "title": "Code Block",
  "description": "A clean code block that builds its entire theme from a single accent color. Pass code and a hex, it does the rest.",
  "dependencies": [
    "motion",
    "prism-react-renderer",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/code-block.tsx",
      "content": "'use client'\n\nimport { Copy } from 'lucide-react'\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react'\nimport { Highlight, type PrismTheme } from 'prism-react-renderer'\nimport React, { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\n\nimport { cn } from '@/lib/utils'\n\nconst TAP_SPRING = { type: 'spring', stiffness: 500, damping: 30 } as const\nconst SWAP_SPRING = { type: 'spring', duration: 0.3, bounce: 0 } as const\nconst CHECK_SPRING = { type: 'spring', duration: 0.4, bounce: 0.35 } as const\nconst COPY_RESET_MS = 1800\n\nexport type CodeBlockProps = Omit<React.ComponentProps<'div'>, 'children'> & {\n    /** The source code to render. */\n    code: string\n    /** Prism language id, e.g. \"tsx\", \"css\", \"json\", \"bash\". */\n    language?: string\n    /** Any hex color. The whole theme is built from shades of it. */\n    accent?: string\n    /** \"auto\" follows the page theme; pass \"dark\" or \"light\" to pin it. */\n    mode?: 'auto' | 'dark' | 'light'\n    /** Filename or path shown in the header. Falls back to the language id when omitted. */\n    filename?: string\n    /** Show the outer frame — background, border, rounded corners, and header. Turn off to render just the code. */\n    showFrame?: boolean\n    /** Show the header bar. Ignored when the frame is off. */\n    showHeader?: boolean\n    /** Show the line-number gutter. */\n    showLineNumbers?: boolean\n    /** Show the copy-to-clipboard button. */\n    showCopyButton?: boolean\n    /** Optional 1-based line numbers to highlight with an accent wash. Off when omitted. */\n    highlightLines?: number[]\n}\nfunction resolvePageMode(): 'dark' | 'light' {\n    const root = document.documentElement\n    if (root.classList.contains('dark')) return 'dark'\n    if (root.classList.contains('light')) return 'light'\n    const attr = root.getAttribute('data-theme')\n    if (attr === 'dark') return 'dark'\n    if (attr === 'light') return 'light'\n    // jsdom has no matchMedia\n    if (typeof window.matchMedia !== 'function') return 'dark'\n    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n}\n\nfunction subscribeToPageMode(onChange: () => void) {\n    const observer = new MutationObserver(onChange)\n    observer.observe(document.documentElement, {\n        attributes: true,\n        attributeFilter: ['class', 'data-theme'],\n    })\n    const media =\n        typeof window.matchMedia === 'function'\n            ? window.matchMedia('(prefers-color-scheme: dark)')\n            : null\n    // old Safari doesn't have addEventListener here\n    media?.addEventListener?.('change', onChange)\n    return () => {\n        observer.disconnect()\n        media?.removeEventListener?.('change', onChange)\n    }\n}\n\n// the server can't know the theme, so assume dark\nconst serverMode = () => 'dark' as const\n\nconst FALLBACK_HSL: [number, number, number] = [211, 100, 52]\n\nfunction hexToHsl(hex: string): [number, number, number] {\n    if (typeof hex !== 'string') return FALLBACK_HSL\n    let value = hex.replace('#', '')\n    if (value.length === 4 || value.length === 8) {\n        value = value.slice(0, value.length === 4 ? 3 : 6)\n    }\n    if (value.length === 3) {\n        value = value.split('').map((c) => c + c).join('')\n    }\n    const r = parseInt(value.slice(0, 2), 16) / 255\n    const g = parseInt(value.slice(2, 4), 16) / 255\n    const b = parseInt(value.slice(4, 6), 16) / 255\n    if (value.length !== 6 || [r, g, b].some(Number.isNaN)) return FALLBACK_HSL\n\n    const max = Math.max(r, g, b)\n    const min = Math.min(r, g, b)\n    const l = (max + min) / 2\n    if (max === min) return [0, 0, l * 100]\n\n    const d = max - min\n    const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)\n    let h: number\n    switch (max) {\n        case r:\n            h = ((g - b) / d + (g < b ? 6 : 0)) / 6\n            break\n        case g:\n            h = ((b - r) / d + 2) / 6\n            break\n        default:\n            h = ((r - g) / d + 4) / 6\n    }\n    return [h * 360, s * 100, l * 100]\n}\n\nconst hsl = (h: number, s: number, l: number, a = 1) => {\n    const hue = ((h % 360) + 360) % 360\n    return a === 1\n        ? `hsl(${hue.toFixed(1)} ${s.toFixed(1)}% ${l.toFixed(1)}%)`\n        : `hsl(${hue.toFixed(1)} ${s.toFixed(1)}% ${l.toFixed(1)}% / ${a})`\n}\n\nfunction buildTheme(accent: string, mode: 'dark' | 'light' = 'dark') {\n    const [h, s, l] = hexToHsl(accent)\n    const tint = (lightness: number, sat = s) => hsl(h, sat, lightness)\n    const dark = mode !== 'light'\n    const accentTone = dark\n        ? tint(Math.min(Math.max(l, 56), 70))\n        : tint(Math.min(Math.max(l, 38), 50))\n    // light mode just flips the lightness ramp\n    const ramp = (lightness: number) => (dark ? lightness : 100 - lightness)\n\n    const colors = dark\n        ? {\n              accent: accentTone,\n              // Neutral chrome — matches the Rare UI preview surface (dark --card).\n              bg: 'oklch(0.1822 0 0)',\n              border: 'rgb(255 255 255 / 0.08)',\n              headerBg: 'rgb(255 255 255 / 0.03)',\n              plain: '#ffffff',\n              muted: 'rgb(255 255 255 / 0.6)',\n              gutter: 'rgb(255 255 255 / 0.28)',\n              hoverWash: 'rgb(255 255 255 / 0.08)',\n              floatBg: 'rgb(255 255 255 / 0.05)',\n              selection: hsl(h, s, 58, 0.3),\n              lineWash: hsl(h, s, 58, 0.1),\n          }\n        : {\n              accent: accentTone,\n              bg: 'oklch(0.985 0 0)',\n              border: 'rgb(0 0 0 / 0.08)',\n              headerBg: 'rgb(0 0 0 / 0.03)',\n              plain: '#171717',\n              muted: 'rgb(0 0 0 / 0.6)',\n              gutter: 'rgb(0 0 0 / 0.32)',\n              hoverWash: 'rgb(0 0 0 / 0.06)',\n              floatBg: 'rgb(0 0 0 / 0.04)',\n              selection: hsl(h, s, 45, 0.25),\n              lineWash: hsl(h, s, 45, 0.08),\n          }\n\n    const theme: PrismTheme = {\n        plain: { color: colors.plain, backgroundColor: 'transparent' },\n        styles: [\n            { types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: tint(ramp(42), s * 0.35), fontStyle: 'italic' } },\n            { types: ['punctuation'], style: { color: tint(ramp(62), s * 0.3) } },\n            { types: ['operator', 'combinator'], style: { color: tint(ramp(70), s * 0.4) } },\n            { types: ['keyword', 'selector', 'atrule', 'important', 'tag'], style: { color: accentTone } },\n            { types: ['string', 'char', 'inserted', 'url'], style: { color: tint(ramp(76)) } },\n            { types: ['function'], style: { color: tint(ramp(88), s * 0.5) } },\n            { types: ['attr-name'], style: { color: tint(ramp(78), s * 0.7), fontStyle: 'italic' } },\n            { types: ['number', 'boolean', 'constant', 'symbol', 'deleted'], style: { color: tint(ramp(70)) } },\n            { types: ['class-name', 'maybe-class-name', 'builtin'], style: { color: tint(ramp(93), s * 0.35) } },\n            { types: ['property', 'variable', 'parameter'], style: { color: tint(ramp(97), s * 0.15) } },\n            { types: ['regex'], style: { color: tint(ramp(72), s * 0.6) } },\n        ],\n    }\n\n    return { colors, theme }\n}\n\n/* ------------------------------- copy button ------------------------------- */\n\nfunction CopyButton({ code, floating }: { code: string; floating?: boolean }) {\n    const [copied, setCopied] = useState(false)\n    const timer = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n    useEffect(() => () => {\n        if (timer.current) clearTimeout(timer.current)\n    }, [])\n\n    const copy = useCallback(async () => {\n        try {\n            if (navigator.clipboard?.writeText) {\n                await navigator.clipboard.writeText(code)\n            } else {\n                // Fallback for non-secure contexts where the Clipboard API is unavailable.\n                const area = document.createElement('textarea')\n                area.value = code\n                area.style.position = 'fixed'\n                area.style.opacity = '0'\n                document.body.appendChild(area)\n                area.select()\n                document.execCommand('copy')\n                area.remove()\n            }\n        } catch {\n            return\n        }\n        setCopied(true)\n        if (timer.current) clearTimeout(timer.current)\n        timer.current = setTimeout(() => setCopied(false), COPY_RESET_MS)\n    }, [code])\n\n    const reduceMotion = useReducedMotion()\n    const swap = reduceMotion\n        ? {\n              initial: { opacity: 0 },\n              animate: { opacity: 1 },\n              exit: { opacity: 0 },\n          }\n        : {\n              initial: { opacity: 0, scale: 0.5, filter: 'blur(4px)' },\n              animate: { opacity: 1, scale: 1, filter: 'blur(0px)' },\n              exit: { opacity: 0, scale: 0.5, filter: 'blur(4px)' },\n          }\n\n    return (\n        <motion.button\n            type='button'\n            data-slot='code-block-copy'\n            aria-label={copied ? 'Copied' : 'Copy code'}\n            onClick={copy}\n            whileTap={reduceMotion ? undefined : { scale: 0.9 }}\n            transition={TAP_SPRING}\n            className={cn(\n                'relative grid size-7 place-items-center rounded-lg text-(--cb-gutter) outline-none transition-[background-color,color] duration-150 ease-out hover:bg-(--cb-hover-wash) hover:text-(--cb-plain) focus-visible:ring-2 focus-visible:ring-(--cb-accent)/60',\n                copied &&\n                    'bg-(--cb-accent)/12 text-(--cb-accent) hover:bg-(--cb-accent)/12 hover:text-(--cb-accent)',\n                floating &&\n                    'absolute top-2.5 right-2.5 z-10 border border-(--cb-border) bg-(--cb-float-bg) backdrop-blur-md',\n            )}\n        >\n            <AnimatePresence initial={false}>\n                {copied ? (\n                    <motion.span\n                        key='check'\n                        className='col-start-1 row-start-1'\n                        {...swap}\n                        transition={reduceMotion ? { duration: 0.15 } : CHECK_SPRING}\n                    >\n                        <svg\n                            viewBox='0 0 24 24'\n                            fill='none'\n                            stroke='currentColor'\n                            strokeWidth={2.5}\n                            strokeLinecap='round'\n                            strokeLinejoin='round'\n                            className='size-3.5'\n                            aria-hidden\n                        >\n                            <motion.path\n                                d='M4 12.5l5 5L20 6.5'\n                                initial={reduceMotion ? false : { pathLength: 0 }}\n                                animate={{ pathLength: 1 }}\n                                transition={{ duration: 0.2, ease: 'easeOut', delay: 0.05 }}\n                            />\n                        </svg>\n                    </motion.span>\n                ) : (\n                    <motion.span\n                        key='copy'\n                        className='col-start-1 row-start-1'\n                        {...swap}\n                        transition={reduceMotion ? { duration: 0.15 } : SWAP_SPRING}\n                    >\n                        <Copy className='size-3.5' />\n                    </motion.span>\n                )}\n            </AnimatePresence>\n        </motion.button>\n    )\n}\n\nexport function CodeBlock({\n    code,\n    language = 'tsx',\n    accent = '#F75001',\n    mode = 'auto',\n    filename,\n    showFrame = true,\n    showHeader = true,\n    showLineNumbers = true,\n    showCopyButton = true,\n    highlightLines,\n    className,\n    style,\n    ...props\n}: CodeBlockProps) {\n    // don't crash on bad props\n    const safeLanguage = typeof language === 'string' ? language : 'tsx'\n    const pageMode = useSyncExternalStore(subscribeToPageMode, resolvePageMode, serverMode)\n    const safeMode = mode === 'light' || mode === 'dark' ? mode : pageMode\n    const { colors, theme } = useMemo(() => buildTheme(accent, safeMode), [accent, safeMode])\n    const trimmed = useMemo(() => {\n        const source = typeof code === 'string' ? code : String(code ?? '')\n        return source.replace(/^\\n+/, '').trimEnd()\n    }, [code])\n    const highlighted = useMemo(\n        () => new Set(Array.isArray(highlightLines) ? highlightLines : []),\n        [highlightLines],\n    )\n\n    const cssVars = {\n        '--cb-accent': colors.accent,\n        '--cb-bg': colors.bg,\n        '--cb-border': colors.border,\n        '--cb-header-bg': colors.headerBg,\n        '--cb-plain': colors.plain,\n        '--cb-muted': colors.muted,\n        '--cb-gutter': colors.gutter,\n        '--cb-hover-wash': colors.hoverWash,\n        '--cb-float-bg': colors.floatBg,\n        '--cb-selection': colors.selection,\n        '--cb-line-wash': colors.lineWash,\n    } as React.CSSProperties\n\n    return (\n        <div\n            data-slot='code-block'\n            className={cn(\n                'group relative flex flex-col overflow-hidden text-left',\n                showFrame && 'rounded-2xl border border-(--cb-border) bg-(--cb-bg)',\n                className,\n            )}\n            style={{ ...cssVars, ...style }}\n            {...props}\n        >\n            {showFrame && showHeader && (\n                <div\n                    data-slot='code-block-header'\n                    className='flex h-10 shrink-0 items-center gap-3 border-b border-(--cb-border) bg-(--cb-header-bg) px-3.5 backdrop-blur-md'\n                >\n                    <span className='min-w-0 flex-1 truncate font-mono text-xs text-(--cb-muted)'>\n                        {filename ?? safeLanguage}\n                    </span>\n                    {showCopyButton && <CopyButton code={trimmed} />}\n                </div>\n            )}\n\n            {!(showFrame && showHeader) && showCopyButton && <CopyButton code={trimmed} floating />}\n\n            <div\n                data-slot='code-block-viewport'\n                role='region'\n                aria-label={filename ?? `${safeLanguage} code`}\n                tabIndex={0}\n                className={cn(\n                    'min-h-0 flex-1 overflow-auto outline-none selection:bg-(--cb-selection) focus-visible:ring-2 focus-visible:ring-(--cb-accent)/40 [scrollbar-width:thin] [scrollbar-color:var(--cb-border)_transparent]',\n                    showFrame && 'py-3',\n                )}\n            >\n                <Highlight code={trimmed} language={safeLanguage} theme={theme}>\n                    {({ tokens, getLineProps, getTokenProps }) => {\n                        const gutterWidth = `${String(tokens.length).length}ch`\n                        return (\n                            <pre\n                                data-slot='code-block-pre'\n                                className='w-max min-w-full font-mono text-[13px] leading-6 [tab-size:4]'\n                            >\n                                {tokens.map((line, i) => {\n                                    const lineProps = getLineProps({ line })\n                                    return (\n                                        <div\n                                            key={i}\n                                            {...lineProps}\n                                            className={cn(\n                                                'relative flex min-w-full',\n                                                showFrame && 'px-3.5',\n                                                highlighted.has(i + 1) && 'bg-(--cb-line-wash)',\n                                                lineProps.className,\n                                            )}\n                                        >\n                                            {showLineNumbers && (\n                                                <span\n                                                    aria-hidden\n                                                    className='mr-4 shrink-0 text-right text-(--cb-gutter) select-none'\n                                                    style={{ width: gutterWidth }}\n                                                >\n                                                    {i + 1}\n                                                </span>\n                                            )}\n                                            <span className='pr-3.5'>\n                                                {line.map((token, key) => (\n                                                    <span key={key} {...getTokenProps({ token })} />\n                                                ))}\n                                            </span>\n                                        </div>\n                                    )\n                                })}\n                            </pre>\n                        )\n                    }}\n                </Highlight>\n            </div>\n        </div>\n    )\n}\n\nexport default CodeBlock\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}