{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "canvas-text",
  "type": "registry:block",
  "title": "Canvas text",
  "description": "Canvas text",
  "files": [
    {
      "path": "components/usages/canvastextusage.tsx",
      "content": "\"use client\";\nimport { cn } from \"@/registry/utilities/cn\";\nimport { CanvasText } from \"@/registry/open-source/canvas-text\";\n\nexport function CanvasTextDemo() {\n    return (\n        <div className=\"flex min-h-80 items-center justify-center p-8\">\n            <h2\n                className={cn(\n                    \"group relative mx-auto mt-4 max-w-2xl text-left text-4xl leading-20 font-bold tracking-tight text-balance text-neutral-600 sm:text-5xl md:text-6xl xl:text-7xl dark:text-neutral-700\",\n                )}\n            >\n                Ship landing pages at{\" \"}\n                <CanvasText\n                    text=\"Lightning Speed\"\n                    backgroundClassName=\"bg-blue-600 dark:bg-blue-700\"\n                    colors={[\n                        \"rgba(0, 153, 255, 1)\",\n                        \"rgba(0, 153, 255, 0.9)\",\n                        \"rgba(0, 153, 255, 0.8)\",\n                        \"rgba(0, 153, 255, 0.7)\",\n                        \"rgba(0, 153, 255, 0.6)\",\n                        \"rgba(0, 153, 255, 0.5)\",\n                        \"rgba(0, 153, 255, 0.4)\",\n                        \"rgba(0, 153, 255, 0.3)\",\n                        \"rgba(0, 153, 255, 0.2)\",\n                        \"rgba(0, 153, 255, 0.1)\",\n                    ]}\n                    lineGap={4}\n                    animationDuration={20}\n                />\n            </h2>\n        </div>\n    );\n}\n",
      "type": "registry:block",
      "target": "~/example.tsx"
    },
    {
      "path": "components/usages/canvastextusage.tsx",
      "content": "\"use client\";\nimport { cn } from \"@/registry/utilities/cn\";\nimport { CanvasText } from \"@/registry/open-source/canvas-text\";\n\nexport function CanvasTextDemo() {\n    return (\n        <div className=\"flex min-h-80 items-center justify-center p-8\">\n            <h2\n                className={cn(\n                    \"group relative mx-auto mt-4 max-w-2xl text-left text-4xl leading-20 font-bold tracking-tight text-balance text-neutral-600 sm:text-5xl md:text-6xl xl:text-7xl dark:text-neutral-700\",\n                )}\n            >\n                Ship landing pages at{\" \"}\n                <CanvasText\n                    text=\"Lightning Speed\"\n                    backgroundClassName=\"bg-blue-600 dark:bg-blue-700\"\n                    colors={[\n                        \"rgba(0, 153, 255, 1)\",\n                        \"rgba(0, 153, 255, 0.9)\",\n                        \"rgba(0, 153, 255, 0.8)\",\n                        \"rgba(0, 153, 255, 0.7)\",\n                        \"rgba(0, 153, 255, 0.6)\",\n                        \"rgba(0, 153, 255, 0.5)\",\n                        \"rgba(0, 153, 255, 0.4)\",\n                        \"rgba(0, 153, 255, 0.3)\",\n                        \"rgba(0, 153, 255, 0.2)\",\n                        \"rgba(0, 153, 255, 0.1)\",\n                    ]}\n                    lineGap={4}\n                    animationDuration={20}\n                />\n            </h2>\n        </div>\n    );\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/utilities/cn.ts",
      "content": "import { ClassValue, clsx } from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\nexport function cn(...inputs: ClassValue[]) {\r\n\treturn twMerge(clsx(inputs));\r\n}\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/open-source/canvas-text.tsx",
      "content": "\"use client\";\nimport React, { useEffect, useRef, useState, useCallback } from \"react\";\nimport { cn } from \"../utilities/cn\";\n\n// Credit:\n// https://ui.aceternity.com/components/canvas-text\n\ninterface CanvasTextProps {\n    text: string;\n    className?: string;\n    backgroundClassName?: string;\n    colors?: string[];\n    animationDuration?: number;\n    lineWidth?: number;\n    lineGap?: number;\n    curveIntensity?: number;\n    overlay?: boolean;\n}\n\nfunction resolveColor(color: string): string {\n    if (color.startsWith(\"var(\")) {\n        const varName = color.slice(4, -1).trim();\n        const resolved = getComputedStyle(document.documentElement)\n            .getPropertyValue(varName)\n            .trim();\n        return resolved || color;\n    }\n    return color;\n}\n\nexport function CanvasText({\n    text,\n    className = \"\",\n    backgroundClassName = \"bg-white dark:bg-neutral-950\",\n    colors = [\"#ff6b6b\", \"#4ecdc4\", \"#45b7d1\", \"#96ceb4\", \"#ffeaa7\", \"#dfe6e9\"],\n    animationDuration = 5,\n    lineWidth = 1.5,\n    lineGap = 10,\n    curveIntensity = 60,\n    overlay = false,\n}: CanvasTextProps) {\n    const canvasRef = useRef<HTMLCanvasElement>(null);\n    const textRef = useRef<HTMLSpanElement>(null);\n    const bgRef = useRef<HTMLSpanElement>(null);\n    const animationRef = useRef<number>(0);\n    const startTimeRef = useRef<number>(0);\n    const [bgColor, setBgColor] = useState(\"#0a0a0a\");\n    const [resolvedColors, setResolvedColors] = useState<string[]>([]);\n    const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n    const [font, setFont] = useState(\"\");\n\n    const updateColors = useCallback(() => {\n        if (bgRef.current) {\n            const computed = window.getComputedStyle(bgRef.current);\n            setBgColor(computed.backgroundColor);\n        }\n        const resolved = colors.map(resolveColor);\n        setResolvedColors(resolved);\n    }, [colors]);\n\n    useEffect(() => {\n        updateColors();\n\n        const observer = new MutationObserver(updateColors);\n        observer.observe(document.documentElement, {\n            attributes: true,\n            attributeFilter: [\"class\"],\n        });\n\n        return () => observer.disconnect();\n    }, [updateColors]);\n\n    useEffect(() => {\n        const textEl = textRef.current;\n        if (!textEl) return;\n\n        const updateDimensions = () => {\n            const rect = textEl.getBoundingClientRect();\n            const computed = window.getComputedStyle(textEl);\n            setDimensions({\n                width: Math.ceil(rect.width) || 400,\n                height: Math.ceil(rect.height) || 200,\n            });\n            setFont(\n                `${computed.fontWeight} ${computed.fontSize} ${computed.fontFamily}`,\n            );\n        };\n\n        updateDimensions();\n\n        const resizeObserver = new ResizeObserver(updateDimensions);\n        resizeObserver.observe(textEl);\n\n        return () => resizeObserver.disconnect();\n    }, [text, className]);\n\n    useEffect(() => {\n        const canvas = canvasRef.current;\n        if (\n            !canvas ||\n            resolvedColors.length === 0 ||\n            dimensions.width === 0 ||\n            !font\n        )\n            return;\n\n        const ctx = canvas.getContext(\"2d\", { alpha: true });\n        if (!ctx) return;\n\n        const { width, height } = dimensions;\n        const dpr = window.devicePixelRatio || 1;\n\n        canvas.width = width * dpr;\n        canvas.height = height * dpr;\n\n        ctx.font = font;\n        const metrics = ctx.measureText(text);\n        const ascent = metrics.actualBoundingBoxAscent;\n        const descent = metrics.actualBoundingBoxDescent;\n        const baselineY = (height + ascent - descent) / 2;\n\n        const numLines = Math.floor(height / lineGap) + 10;\n        startTimeRef.current = performance.now();\n\n        const animate = (currentTime: number) => {\n            const elapsed = (currentTime - startTimeRef.current) / 1000;\n            const phase = (elapsed / animationDuration) * Math.PI * 2;\n\n            ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n            ctx.clearRect(0, 0, width, height);\n\n            ctx.globalCompositeOperation = \"source-over\";\n            ctx.font = font;\n            ctx.textBaseline = \"alphabetic\";\n            ctx.textAlign = \"left\";\n            ctx.fillStyle = \"#000\";\n            ctx.fillText(text, 0, baselineY);\n\n            ctx.globalCompositeOperation = \"source-in\";\n            ctx.fillStyle = bgColor;\n            ctx.fillRect(0, 0, width, height);\n\n            ctx.globalCompositeOperation = \"source-atop\";\n            for (let i = 0; i < numLines; i++) {\n                const y = i * lineGap;\n\n                const curve1 = Math.sin(phase) * curveIntensity;\n                const curve2 = Math.sin(phase + 0.5) * curveIntensity * 0.6;\n\n                const colorIndex = i % resolvedColors.length;\n                ctx.strokeStyle = resolvedColors[colorIndex];\n                ctx.lineWidth = lineWidth;\n\n                ctx.beginPath();\n                ctx.moveTo(0, y);\n                ctx.bezierCurveTo(\n                    width * 0.33,\n                    y + curve1,\n                    width * 0.66,\n                    y + curve2,\n                    width,\n                    y,\n                );\n                ctx.stroke();\n            }\n\n            animationRef.current = requestAnimationFrame(animate);\n        };\n\n        animationRef.current = requestAnimationFrame(animate);\n\n        return () => {\n            cancelAnimationFrame(animationRef.current);\n        };\n    }, [\n        text,\n        font,\n        bgColor,\n        resolvedColors,\n        animationDuration,\n        lineWidth,\n        lineGap,\n        curveIntensity,\n        dimensions,\n    ]);\n\n    return (\n        <span\n            className={cn(\n                \"relative inline-block\",\n                overlay && \"absolute inset-0\",\n                className,\n            )}\n        >\n            <span\n                ref={bgRef}\n                className={cn(\n                    \"pointer-events-none absolute h-0 w-0 opacity-0\",\n                    backgroundClassName,\n                )}\n                aria-hidden=\"true\"\n            />\n            <span ref={textRef} className=\"invisible inline-block\" aria-hidden=\"true\">\n                {text}\n            </span>\n            <canvas\n                ref={canvasRef}\n                className=\"pointer-events-none absolute top-0 left-0\"\n                style={{\n                    width: dimensions.width || \"auto\",\n                    height: dimensions.height || \"auto\",\n                }}\n                aria-label={text}\n                role=\"img\"\n            />\n        </span>\n    );\n}\n",
      "type": "registry:ui"
    }
  ]
}