{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "nucleus-text",
  "title": "Nucleus Text",
  "description": "Text assembled from particles that fly inward, settle into letters and gently jitter.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/nucleus-text.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NucleusTextProps extends React.HTMLAttributes<HTMLSpanElement> {\n  ref?: React.Ref<HTMLSpanElement>;\n  children: string;\n  color?: string;\n  particleColor?: string;\n  particleCount?: number;\n  particleSize?: number;\n  gatherSpeed?: number;\n  shake?: number;\n}\n\ntype Particle = {\n  sx: number;\n  sy: number;\n  tx: number;\n  ty: number;\n  delay: number;\n  duration: number;\n  size: number;\n  drift: number;\n};\n\nfunction NucleusText({\n  ref,\n  children,\n  color = \"#b8ff8b\",\n  particleColor = color,\n  particleCount = 90,\n  particleSize = 1.6,\n  gatherSpeed = 1,\n  shake = 1,\n  className,\n  style,\n  ...props\n}: NucleusTextProps) {\n  const hostRef = React.useRef<HTMLSpanElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n\n  React.useImperativeHandle(ref, () => hostRef.current as HTMLSpanElement);\n\n  React.useEffect(() => {\n    const host = hostRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\", { alpha: true });\n    if (!host || !canvas || !ctx) return;\n\n    const reduceMotion = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    if (reduceMotion) return;\n\n    let raf = 0;\n    let disposed = false;\n    const padding = 76;\n    const rect = host.getBoundingClientRect();\n    const width = Math.max(1, rect.width);\n    const height = Math.max(1, rect.height);\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const canvasWidth = width + padding * 2;\n    const canvasHeight = height + padding * 2;\n    const durationScale = 1 / Math.max(0.25, gatherSpeed);\n    const count = Math.max(12, Math.min(320, Math.floor(particleCount)));\n    const random = (min: number, max: number) => min + Math.random() * (max - min);\n\n    canvas.width = Math.round(canvasWidth * dpr);\n    canvas.height = Math.round(canvasHeight * dpr);\n    canvas.style.width = `${canvasWidth}px`;\n    canvas.style.height = `${canvasHeight}px`;\n    canvas.style.left = `${-padding}px`;\n    canvas.style.top = `${-padding}px`;\n\n    const particles: Particle[] = Array.from({ length: count }, () => {\n      const side = Math.floor(Math.random() * 4);\n      let sx = 0;\n      let sy = 0;\n      if (side === 0) {\n        sx = random(0, canvasWidth);\n        sy = random(0, padding * 0.8);\n      } else if (side === 1) {\n        sx = random(0, canvasWidth);\n        sy = random(canvasHeight - padding * 0.8, canvasHeight);\n      } else if (side === 2) {\n        sx = random(0, padding * 0.8);\n        sy = random(0, canvasHeight);\n      } else {\n        sx = random(canvasWidth - padding * 0.8, canvasWidth);\n        sy = random(0, canvasHeight);\n      }\n\n      return {\n        sx,\n        sy,\n        tx: padding + random(width * 0.04, width * 0.96),\n        ty: padding + random(height * 0.12, height * 0.88),\n        delay: random(0, 420) * durationScale,\n        duration: random(520, 1050) * durationScale,\n        size: random(particleSize * 0.55, particleSize * 1.5),\n        drift: random(-10, 10),\n      };\n    });\n\n    const start = performance.now();\n    let shakeAnimation: Animation | undefined;\n    if (shake > 0) {\n      shakeAnimation = host.animate(\n        [\n          { transform: \"translate3d(0, 0, 0)\" },\n          { transform: `translate3d(${-2.5 * shake}px, 0, 0) rotate(${-0.45 * shake}deg)` },\n          { transform: `translate3d(${2.5 * shake}px, 0, 0) rotate(${0.45 * shake}deg)` },\n          { transform: `translate3d(${-1.25 * shake}px, 0, 0)` },\n          { transform: \"translate3d(0, 0, 0)\" },\n        ],\n        { duration: 360, easing: \"ease-out\", iterations: 1 },\n      );\n    }\n\n    const frame = (now: number) => {\n      if (disposed) return;\n      const elapsed = now - start;\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n      ctx.save();\n      ctx.scale(dpr, dpr);\n\n      let activeParticles = 0;\n      for (const p of particles) {\n        const t = (elapsed - p.delay) / p.duration;\n        if (t <= 0 || t >= 1) continue;\n        activeParticles++;\n        const eased = 1 - Math.pow(1 - t, 3);\n        const x = p.sx + (p.tx - p.sx) * eased + Math.sin(t * Math.PI) * p.drift;\n        const y = p.sy + (p.ty - p.sy) * eased;\n        const alpha = Math.min(1, t * 5) * Math.pow(1 - t, 1.7);\n\n        ctx.globalAlpha = alpha;\n        ctx.fillStyle = particleColor;\n        ctx.shadowColor = particleColor;\n        ctx.shadowBlur = 5;\n        ctx.beginPath();\n        ctx.arc(x, y, p.size, 0, Math.PI * 2);\n        ctx.fill();\n      }\n\n      ctx.restore();\n      if (activeParticles > 0 || elapsed < 1500 * durationScale) {\n        raf = requestAnimationFrame(frame);\n      } else {\n        ctx.clearRect(0, 0, canvas.width, canvas.height);\n      }\n    };\n\n    raf = requestAnimationFrame(frame);\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      shakeAnimation?.cancel();\n    };\n  }, [children, particleColor, particleCount, particleSize, gatherSpeed, shake]);\n\n  return (\n    <span\n      ref={hostRef}\n      className={cn(\"relative inline-block align-middle\", className)}\n      style={{ ...style, color, lineHeight: style?.lineHeight ?? 1.05 }}\n      {...props}\n    >\n      <span className=\"relative z-0\">{children}</span>\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute z-10 max-w-none\"\n      />\n    </span>\n  );\n}\n\nexport { NucleusText };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}