{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spark-waves",
  "type": "registry:ui",
  "title": "Spark Waves",
  "description": "Radial spark waves pulse outward and inward, creating a dynamic energy-driven background layer.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/spark-waves.tsx",
      "content": "\"use client\";\nimport React, { useRef, useEffect, useCallback } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SparkWavesProps {\n  sparkColor?: string;\n  sparkSize?: number;\n  sparkCount?: number;\n  duration?: number;\n  easing?: \"linear\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\";\n  waveInterval?: number;\n  maxRadius?: number;\n  ringsPerWave?: number;\n  ringSpacing?: number;\n  enableInward?: boolean;\n  className?: string;\n  children?: React.ReactNode;\n}\n\ninterface Spark {\n  x: number;\n  y: number;\n  angle: number;\n  radius: number;\n  startTime: number;\n  waveId: number;\n  ringIndex: number;\n  direction: \"outward\" | \"inward\";\n}\n\nexport const SparkWaves: React.FC<SparkWavesProps> = ({\n  sparkColor = \"#3b82f6\",\n  sparkSize = 10,\n  sparkCount = 24,\n  duration = 4000,\n  easing = \"ease-out\",\n  waveInterval = 1400,\n  maxRadius = 1000,\n  ringsPerWave = 6,\n  ringSpacing = 50,\n  enableInward = true,\n  className = \"\",\n  children,\n}) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const sparksRef = useRef<Spark[]>([]);\n  const lastWaveTimeRef = useRef<number>(0);\n  const lastInwardWaveTimeRef = useRef<number>(700);\n  const waveIdRef = useRef<number>(0);\n  const centerXRef = useRef<number>(0);\n  const centerYRef = useRef<number>(0);\n  const maxScreenRadiusRef = useRef<number>(0);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const parent = canvas.parentElement;\n    if (!parent) return;\n\n    let resizeTimeout: NodeJS.Timeout;\n\n    const resizeCanvas = () => {\n      const { width, height } = parent.getBoundingClientRect();\n      if (canvas.width !== width || canvas.height !== height) {\n        canvas.width = width;\n        canvas.height = height;\n        centerXRef.current = width / 2;\n        centerYRef.current = height / 2;\n        maxScreenRadiusRef.current =\n          Math.sqrt(width * width + height * height) / 2 + 100;\n      }\n    };\n\n    const handleResize = () => {\n      clearTimeout(resizeTimeout);\n      resizeTimeout = setTimeout(resizeCanvas, 100);\n    };\n\n    const ro = new ResizeObserver(handleResize);\n    ro.observe(parent);\n\n    resizeCanvas();\n\n    return () => {\n      ro.disconnect();\n      clearTimeout(resizeTimeout);\n    };\n  }, []);\n\n  const easeFunc = useCallback(\n    (t: number) => {\n      switch (easing) {\n        case \"linear\":\n          return t;\n        case \"ease-in\":\n          return t * t;\n        case \"ease-in-out\":\n          return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;\n        default:\n          return t * (2 - t);\n      }\n    },\n    [easing]\n  );\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    let animationId: number;\n\n    const draw = (timestamp: number) => {\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n      if (timestamp - lastWaveTimeRef.current >= waveInterval) {\n        const currentWaveId = waveIdRef.current++;\n\n        for (let ringIndex = 0; ringIndex < ringsPerWave; ringIndex++) {\n          const ringDelay = ringIndex * 150;\n\n          for (let i = 0; i < sparkCount; i++) {\n            const angleOffset = ringIndex % 2 === 0 ? 0 : Math.PI / sparkCount;\n            const angle = (2 * Math.PI * i) / sparkCount + angleOffset;\n\n            sparksRef.current.push({\n              x: centerXRef.current,\n              y: centerYRef.current,\n              angle: angle,\n              radius: ringIndex * ringSpacing,\n              startTime: timestamp + ringDelay,\n              waveId: currentWaveId,\n              ringIndex: ringIndex,\n              direction: \"outward\",\n            });\n          }\n        }\n\n        lastWaveTimeRef.current = timestamp;\n      }\n\n      if (\n        enableInward &&\n        timestamp - lastInwardWaveTimeRef.current >= waveInterval\n      ) {\n        const currentWaveId = waveIdRef.current++;\n\n        for (let ringIndex = 0; ringIndex < ringsPerWave; ringIndex++) {\n          const ringDelay = ringIndex * 150;\n\n          for (let i = 0; i < sparkCount; i++) {\n            const angleOffset =\n              ringIndex % 2 === 0 ? Math.PI / sparkCount / 2 : 0;\n            const angle = (2 * Math.PI * i) / sparkCount + angleOffset;\n\n            sparksRef.current.push({\n              x: centerXRef.current,\n              y: centerYRef.current,\n              angle: angle,\n              radius: maxScreenRadiusRef.current - ringIndex * ringSpacing,\n              startTime: timestamp + ringDelay,\n              waveId: currentWaveId,\n              ringIndex: ringIndex,\n              direction: \"inward\",\n            });\n          }\n        }\n\n        lastInwardWaveTimeRef.current = timestamp;\n      }\n\n      sparksRef.current = sparksRef.current.filter((spark: Spark) => {\n        const elapsed = timestamp - spark.startTime;\n\n        if (elapsed < 0) return true;\n        if (elapsed >= duration) return false;\n\n        const progress = elapsed / duration;\n        const eased = easeFunc(progress);\n\n        let currentRadius: number;\n\n        if (spark.direction === \"outward\") {\n          const expansionDistance = eased * maxRadius;\n          currentRadius = spark.radius + expansionDistance;\n        } else {\n          const contractionDistance = eased * (maxScreenRadiusRef.current - 0);\n          currentRadius = spark.radius - contractionDistance;\n\n          if (currentRadius < 0) return false;\n        }\n\n        const fadeStart = 0.6;\n        const opacity =\n          progress < fadeStart\n            ? 1\n            : 1 - (progress - fadeStart) / (1 - fadeStart);\n\n        const lineLength = sparkSize * (1 - eased * 0.3);\n\n        const x1 = spark.x + currentRadius * Math.cos(spark.angle);\n        const y1 = spark.y + currentRadius * Math.sin(spark.angle);\n\n        const lineAngle =\n          spark.direction === \"inward\" ? spark.angle + Math.PI : spark.angle;\n        const x2 = x1 + lineLength * Math.cos(lineAngle);\n        const y2 = y1 + lineLength * Math.sin(lineAngle);\n\n        ctx.shadowBlur = 10;\n        ctx.shadowColor = sparkColor;\n        ctx.strokeStyle = sparkColor;\n        ctx.globalAlpha = opacity;\n        ctx.lineWidth = 2.5;\n        ctx.lineCap = \"round\";\n        ctx.beginPath();\n        ctx.moveTo(x1, y1);\n        ctx.lineTo(x2, y2);\n        ctx.stroke();\n\n        return true;\n      });\n\n      ctx.shadowBlur = 0;\n      ctx.globalAlpha = 1;\n      animationId = requestAnimationFrame(draw);\n    };\n\n    animationId = requestAnimationFrame(draw);\n\n    return () => {\n      cancelAnimationFrame(animationId);\n    };\n  }, [\n    sparkColor,\n    sparkSize,\n    sparkCount,\n    duration,\n    easeFunc,\n    waveInterval,\n    maxRadius,\n    ringsPerWave,\n    ringSpacing,\n    enableInward,\n  ]);\n\n  return (\n    <div className={cn(\"relative overflow-hidden\", className)}>\n      <canvas ref={canvasRef} className=\"absolute inset-0 w-full h-full\" />\n      {children && <div className=\"relative z-10\">{children}</div>}\n    </div>\n  );\n};\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
