{
  "name": "canvas-text",
  "type": "registry:ui",
  "files": [
    {
      "path": "components/ui/canvas-text.tsx",
      "content": "\"use client\";\nimport { cn } from \"@/lib/utils\";\nimport React, { useEffect, useRef, useState, useCallback } from \"react\";\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",
      "target": "components/ui/canvas-text.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "Canvas Text"
}