Fortura/apps/www/public/r/code-editor.json
2025-08-20 04:12:49 -06:00

22 lines
7.0 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "code-editor",
"type": "registry:ui",
"title": "Code Editor",
"description": "A code editor component featuring syntax highlighting and animation.",
"dependencies": [
"motion",
"shiki"
],
"registryDependencies": [
"https://animate-ui.com/r/copy-button"
],
"files": [
{
"path": "registry/components/code-editor/index.tsx",
"content": "'use client';\n\nimport * as React from 'react';\nimport { useInView, type UseInViewOptions } from 'motion/react';\nimport { useTheme } from 'next-themes';\n\nimport { cn } from '@/lib/utils';\nimport { CopyButton } from '@/components/animate-ui/buttons/copy';\n\ntype CodeEditorProps = Omit<React.ComponentProps<'div'>, 'onCopy'> & {\n children: string;\n lang: string;\n themes?: {\n light: string;\n dark: string;\n };\n duration?: number;\n delay?: number;\n header?: boolean;\n dots?: boolean;\n icon?: React.ReactNode;\n cursor?: boolean;\n inView?: boolean;\n inViewMargin?: UseInViewOptions['margin'];\n inViewOnce?: boolean;\n copyButton?: boolean;\n writing?: boolean;\n title?: string;\n onDone?: () => void;\n onCopy?: (content: string) => void;\n};\n\nfunction CodeEditor({\n children: code,\n lang,\n themes = {\n light: 'github-light',\n dark: 'github-dark',\n },\n duration = 5,\n delay = 0,\n className,\n header = true,\n dots = true,\n icon,\n cursor = false,\n inView = false,\n inViewMargin = '0px',\n inViewOnce = true,\n copyButton = false,\n writing = true,\n title,\n onDone,\n onCopy,\n ...props\n}: CodeEditorProps) {\n const { resolvedTheme } = useTheme();\n\n const editorRef = React.useRef<HTMLDivElement>(null);\n const [visibleCode, setVisibleCode] = React.useState('');\n const [highlightedCode, setHighlightedCode] = React.useState('');\n const [isDone, setIsDone] = React.useState(false);\n\n const inViewResult = useInView(editorRef, {\n once: inViewOnce,\n margin: inViewMargin,\n });\n const isInView = !inView || inViewResult;\n\n React.useEffect(() => {\n if (!visibleCode.length || !isInView) return;\n\n const loadHighlightedCode = async () => {\n try {\n const { codeToHtml } = await import('shiki');\n\n const highlighted = await codeToHtml(visibleCode, {\n lang,\n themes: {\n light: themes.light,\n dark: themes.dark,\n },\n defaultColor: resolvedTheme === 'dark' ? 'dark' : 'light',\n });\n\n setHighlightedCode(highlighted);\n } catch (e) {\n console.error(`Language \"${lang}\" could not be loaded.`, e);\n }\n };\n\n loadHighlightedCode();\n }, [\n lang,\n themes,\n writing,\n isInView,\n duration,\n delay,\n visibleCode,\n resolvedTheme,\n ]);\n\n React.useEffect(() => {\n if (!writing) {\n setVisibleCode(code);\n onDone?.();\n return;\n }\n\n if (!code.length || !isInView) return;\n\n const characters = Array.from(code);\n let index = 0;\n const totalDuration = duration * 1000;\n const interval = totalDuration / characters.length;\n let intervalId: NodeJS.Timeout;\n\n const timeout = setTimeout(() => {\n intervalId = setInterval(() => {\n if (index < characters.length) {\n setVisibleCode((prev) => {\n const currentIndex = index;\n index += 1;\n return prev + characters[currentIndex];\n });\n editorRef.current?.scrollTo({\n top: editorRef.current?.scrollHeight,\n behavior: 'smooth',\n });\n } else {\n clearInterval(intervalId);\n setIsDone(true);\n onDone?.();\n }\n }, interval);\n }, delay * 1000);\n\n return () => {\n clearTimeout(timeout);\n clearInterval(intervalId);\n };\n }, [code, duration, delay, isInView, writing, onDone]);\n\n return (\n <div\n data-slot=\"code-editor\"\n className={cn(\n 'relative bg-muted/50 w-[600px] h-[400px] border border-border overflow-hidden flex flex-col rounded-xl',\n className,\n )}\n {...props}\n >\n {header ? (\n <div className=\"bg-muted border-b border-border/75 dark:border-border/50 relative flex flex-row items-center justify-between gap-y-2 h-10 px-4\">\n {dots && (\n <div className=\"flex flex-row gap-x-2\">\n <div className=\"size-2 rounded-full bg-red-500\"></div>\n <div className=\"size-2 rounded-full bg-yellow-500\"></div>\n <div className=\"size-2 rounded-full bg-green-500\"></div>\n </div>\n )}\n\n {title && (\n <div\n className={cn(\n 'flex flex-row items-center gap-2',\n dots &&\n 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',\n )}\n >\n {icon ? (\n <div\n className=\"text-muted-foreground [&_svg]:size-3.5\"\n dangerouslySetInnerHTML={\n typeof icon === 'string' ? { __html: icon } : undefined\n }\n >\n {typeof icon !== 'string' ? icon : null}\n </div>\n ) : null}\n <figcaption className=\"flex-1 truncate text-muted-foreground text-[13px]\">\n {title}\n </figcaption>\n </div>\n )}\n\n {copyButton ? (\n <CopyButton\n content={code}\n size=\"sm\"\n variant=\"ghost\"\n className=\"-me-2 bg-transparent hover:bg-black/5 dark:hover:bg-white/10\"\n onCopy={onCopy}\n />\n ) : null}\n </div>\n ) : (\n copyButton && (\n <CopyButton\n content={code}\n size=\"sm\"\n variant=\"ghost\"\n className=\"absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10\"\n onCopy={onCopy}\n />\n )\n )}\n <div\n ref={editorRef}\n className=\"h-[calc(100%-2.75rem)] w-full text-sm p-4 font-mono relative overflow-auto flex-1\"\n >\n <div\n className={cn(\n '[&>pre,_&_code]:!bg-transparent [&>pre,_&_code]:[background:transparent_!important] [&>pre,_&_code]:border-none [&_code]:!text-[13px]',\n cursor &&\n !isDone &&\n \"[&_.line:last-of-type::after]:content-['|'] [&_.line:last-of-type::after]:animate-pulse [&_.line:last-of-type::after]:inline-block [&_.line:last-of-type::after]:w-[1ch] [&_.line:last-of-type::after]:-translate-px\",\n )}\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n />\n </div>\n </div>\n );\n}\n\nexport { CodeEditor, type CodeEditorProps };\n",
"type": "registry:ui",
"target": "components/animate-ui/components/code-editor.tsx"
}
]
}