"use client"; import React, { useState, useMemo } from "react"; import { CheckIcon, CopyIcon } from "@radix-ui/react-icons"; import { cn } from "@/lib/utils"; export interface CodeBlockProps { code?: string; language?: string; color?: string; showLineNumbers?: boolean; className?: string; filename?: string; } const DEFAULT_CODE = `import { useSpring, animated } from "motion/react"; type OrbitProps = { radius?: number; speed?: number; }; export function Orbit({ radius = 120, speed = 1 }: OrbitProps) { const angle = useSpring(0, { stiffness: 80, damping: 20 }); const x = Math.cos(angle.get()) * radius; const y = Math.sin(angle.get()) * radius; return ( ); }`; const KEYWORDS = new Set([ "import", "from", "export", "default", "const", "let", "var", "function", "return", "interface", "type", "extends", "as", "typeof", "keyof", "new", "true", "false", "null", "undefined", "if", "else", "switch", "case", ]); export const CodeBlock = ({ code = DEFAULT_CODE, color = "#4ade80", showLineNumbers = true, className, filename, }: CodeBlockProps) => { const [copied, setCopied] = useState(false); const handleCopy = async () => { try { await navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { setCopied(false); } }; const lines = useMemo(() => { return code.split("\n").map((line, lineIdx) => { const regex = /(".*?"|'.*?'|`.*?`|<\/?[A-Za-z0-9_$.]+|\/?>|[A-Za-z_$][A-Za-z0-9_$]*|0[xX][\da-fA-F]+|\d*\.?\d+|[{}()[\];:,.=><&|!+*/?-]|\s+|.)/gu; const tokens: React.ReactNode[] = []; let match; const isImportLine = line.trimStart().startsWith("import "); while ((match = regex.exec(line)) !== null) { const token = match[0]; const key = `${lineIdx}-${match.index}`; if ( token.startsWith('"') || token.startsWith("'") || token.startsWith("`") ) { if (isImportLine) { tokens.push( {token} , ); } else { tokens.push( {token} , ); } } else if (token.startsWith("<") || token === "/>" || token === ">") { tokens.push( {token} , ); } else if (token === "style" || token === "className") { tokens.push( {token} , ); } else if (KEYWORDS.has(token)) { tokens.push( {token} , ); } else if (/^(?:0[xX][\da-fA-F]+|\d*\.?\d+)$/.test(token)) { tokens.push( {token} , ); } else if (/^[{}()[\];:,.=><&|!+*/?-]+$/.test(token)) { tokens.push( {token} , ); } else if (/^\s+$/.test(token)) { tokens.push({token}); } else { tokens.push( {token} , ); } } return { text: line, tokens, }; }); }, [code, color]); return (
{filename && (
{filename}
)} {copied ? "Copied" : ""}
{lines.map((line, idx) => (
{showLineNumbers && ( {idx + 1} )}
{line.tokens.length > 0 ? line.tokens : "\u00A0"}
))}
); };