"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"}
))}
);
};