{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "code-block", "description": "A beautiful code block component with tabs, copy functionality, and smooth animations", "dependencies": [ "motion" ], "files": [ { "path": "registry/default/ui/code-block.tsx", "content": "\"use client\"\n\nimport { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\"\nimport { Check, Copy } from \"lucide-react\"\nimport { AnimatePresence, motion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface CodeTab {\n label: string\n code: string\n language?: string\n}\n\ninterface CodeBlockProps {\n tabs?: CodeTab[]\n code?: string\n language?: string\n className?: string\n}\n\nexport function CodeBlock({\n tabs,\n code,\n language = \"bash\",\n className,\n}: CodeBlockProps) {\n const [activeTab, setActiveTab] = useState(0)\n const [copied, setCopied] = useState(false)\n const [direction, setDirection] = useState(0)\n const preRef = useRef(null)\n const tabsContainerRef = useRef(null)\n const tabRefs = useRef<(HTMLButtonElement | null)[]>([])\n const [hasOverflow, setHasOverflow] = useState(false)\n const [indicator, setIndicator] = useState<{\n left: number\n width: number\n } | null>(null)\n\n const measureIndicator = useCallback(() => {\n const container = tabsContainerRef.current\n const activeEl = tabRefs.current[activeTab]\n\n if (!container || !activeEl) {\n return\n }\n\n const containerRect = container.getBoundingClientRect()\n const tabRect = activeEl.getBoundingClientRect()\n\n setIndicator({\n left: tabRect.left - containerRect.left,\n width: tabRect.width,\n })\n }, [activeTab])\n\n const codeContent = useMemo(() => {\n if (tabs && tabs.length > 0) {\n return tabs\n }\n if (code) {\n return [{ label: language, code, language }]\n }\n return []\n }, [tabs, code, language])\n\n const currentCode = codeContent[activeTab]?.code || \"\"\n\n // Check overflow when tab changes or content updates\n // biome-ignore lint/correctness/useExhaustiveDependencies: activeTab is needed to recheck overflow when content changes\n useLayoutEffect(() => {\n const checkOverflow = () => {\n if (preRef.current) {\n const hasHorizontalOverflow =\n preRef.current.scrollWidth > preRef.current.clientWidth\n setHasOverflow(hasHorizontalOverflow)\n }\n }\n\n checkOverflow()\n const resizeObserver = new ResizeObserver(checkOverflow)\n if (preRef.current) {\n resizeObserver.observe(preRef.current)\n }\n\n return () => {\n resizeObserver.disconnect()\n }\n }, [activeTab])\n\n useLayoutEffect(() => {\n measureIndicator()\n\n const resizeObserver = new ResizeObserver(measureIndicator)\n const container = tabsContainerRef.current\n\n if (container) {\n resizeObserver.observe(container)\n }\n\n for (const tab of tabRefs.current) {\n if (tab) {\n resizeObserver.observe(tab)\n }\n }\n\n return () => {\n resizeObserver.disconnect()\n }\n }, [measureIndicator])\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(currentCode)\n setCopied(true)\n setTimeout(() => setCopied(false), 2000)\n }\n\n const handleTabChange = (index: number) => {\n setDirection(index > activeTab ? 1 : -1)\n setActiveTab(index)\n }\n\n if (codeContent.length === 0) return null\n\n return (\n \n {/* Tab Bar */}\n {codeContent.length > 1 && (\n
\n \n
\n {codeContent.map((tab, index) => (\n {\n tabRefs.current[index] = element\n }}\n type=\"button\"\n role=\"tab\"\n aria-selected={activeTab === index}\n onClick={() => handleTabChange(index)}\n className={cn(\n \"flex items-center relative gap-1.5 my-1 mb-1.5 outline-0\",\n \"whitespace-nowrap font-medium transition-colors duration-150\",\n \"px-1.5 rounded-lg\",\n \"first:ml-2.5\",\n \"hover:bg-zinc-200/50 dark:hover:bg-zinc-700/70\",\n activeTab === index\n ? \"text-zinc-950 dark:text-zinc-50\"\n : \"text-zinc-500 dark:text-zinc-400\"\n )}\n >\n {tab.label}\n \n ))}\n {indicator && (\n \n )}\n
\n
\n \n )}\n\n {/* Code Content */}\n
\n {/* Copy Button */}\n \n \n \n \n \n \n \n \n \n {copied ? \"Copied\" : \"Copy\"}\n \n 1 ? \"rounded-b-2xl\" : \"rounded-2xl\",\n hasOverflow ? \"overflow-x-auto\" : \"overflow-x-hidden\",\n hasOverflow && \"scrollbar-thin scrollbar-thumb-rounded\",\n hasOverflow &&\n \"scrollbar-thumb-black/15 hover:scrollbar-thumb-black/20\",\n hasOverflow &&\n \"dark:scrollbar-thumb-white/20 dark:hover:scrollbar-thumb-white/25\",\n hasOverflow && \"[&::-webkit-scrollbar]:h-2\",\n hasOverflow && \"[&::-webkit-scrollbar-thumb]:rounded-full\",\n hasOverflow && \"[&::-webkit-scrollbar-thumb]:bg-black/15\",\n hasOverflow && \"[&::-webkit-scrollbar-thumb]:dark:bg-white/20\",\n hasOverflow && \"[&::-webkit-scrollbar-thumb:hover]:bg-black/20\",\n hasOverflow &&\n \"[&::-webkit-scrollbar-thumb:hover]:dark:bg-white/25\",\n hasOverflow && \"[&::-webkit-scrollbar-track]:bg-transparent\"\n )}\n >\n \n 0 ? 20 : -20,\n filter: \"blur(4px)\",\n }}\n animate={{\n opacity: 1,\n x: 0,\n filter: \"blur(0px)\",\n }}\n exit={{\n opacity: 0,\n x: direction > 0 ? -20 : 20,\n filter: \"blur(4px)\",\n }}\n transition={{\n duration: 0.15,\n ease: \"easeOut\",\n }}\n className=\"font-mono text-zinc-950 dark:text-zinc-50 block whitespace-pre\"\n >\n {currentCode}\n \n \n \n
\n \n )\n}\n", "type": "registry:ui" } ], "type": "registry:ui" }