{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dualsparks",
  "type": "registry:ui",
  "title": "Dual Sparks",
  "description": "Corner-based spark waves that radiate inward and outward with smooth motion.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/dualsparks.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef, useCallback } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DualSparksProps {\n  sparkColor?: string;\n  sparkColorDark?: 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  corners?: \"both\" | \"left\" | \"right\";\n  backgroundColor?: string;\n  backgroundColorDark?: string;\n  clearBackground?: boolean;\n  backgroundOpacity?: number;\n  className?: string;\n  children?: React.ReactNode;\n  forceDarkMode?: boolean;\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  corner: \"left\" | \"right\";\n}\n\nexport const DualSparks: React.FC<DualSparksProps> = ({\n  sparkColor = \"#000000\",\n  sparkColorDark = \"#ffffff\",\n  sparkSize = 10,\n  sparkCount = 24,\n  duration = 3500,\n  easing = \"ease-out\",\n  waveInterval = 1400,\n  maxRadius = 800,\n  ringsPerWave = 5,\n  ringSpacing = 40,\n  enableInward = true,\n  corners = \"both\",\n  backgroundColor = \"rgba(240, 240, 250, 0.15)\",\n  backgroundColorDark = \"rgba(0, 0, 0, 0.15)\",\n  clearBackground = true,\n  backgroundOpacity = 0.15,\n  className = \"\",\n  children,\n  forceDarkMode,\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 containerWidthRef = useRef<number>(0);\n  const containerHeightRef = useRef<number>(0);\n  const animationIdRef = useRef<number>(0);\n\n  const isDarkMode = useCallback(() => {\n    if (forceDarkMode !== undefined) return forceDarkMode;\n    if (typeof document === \"undefined\") return false;\n    return document.documentElement.classList.contains(\"dark\");\n  }, [forceDarkMode]);\n\n  const getSparkColor = useCallback(() => {\n    return isDarkMode() ? sparkColorDark : sparkColor;\n  }, [isDarkMode, sparkColor, sparkColorDark]);\n\n  const getBackgroundColor = useCallback(() => {\n    const baseColor = isDarkMode() ? backgroundColorDark : backgroundColor;\n\n    if (backgroundOpacity !== undefined && backgroundOpacity !== 0.15) {\n      const match = baseColor.match(\n        /rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?\\)/\n      );\n      if (match) {\n        const [, r, g, b] = match;\n        return `rgba(${r}, ${g}, ${b}, ${backgroundOpacity})`;\n      }\n    }\n    return baseColor;\n  }, [isDarkMode, backgroundColor, backgroundColorDark, backgroundOpacity]);\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\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        containerWidthRef.current = width;\n        containerHeightRef.current = height;\n\n        sparksRef.current = [];\n        waveIdRef.current = 0;\n        lastWaveTimeRef.current = 0;\n        lastInwardWaveTimeRef.current = 700;\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    window.addEventListener(\"resize\", handleResize);\n\n    resizeCanvas();\n\n    return () => {\n      ro.disconnect();\n      window.removeEventListener(\"resize\", handleResize);\n      clearTimeout(resizeTimeout);\n    };\n  }, []);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\", { alpha: true });\n    if (!ctx) return;\n\n    const animate = (timestamp: number) => {\n      if (clearBackground) {\n        ctx.fillStyle = getBackgroundColor();\n        ctx.fillRect(0, 0, canvas.width, canvas.height);\n      } else {\n        ctx.clearRect(0, 0, canvas.width, canvas.height);\n      }\n\n      const currentSparkColor = getSparkColor();\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            if (corners === \"both\" || corners === \"left\") {\n              sparksRef.current.push({\n                x: 0,\n                y: 0,\n                angle: angle,\n                radius: ringIndex * ringSpacing,\n                startTime: timestamp + ringDelay,\n                waveId: currentWaveId,\n                ringIndex: ringIndex,\n                direction: \"outward\",\n                corner: \"left\",\n              });\n            }\n\n            if (corners === \"both\" || corners === \"right\") {\n              sparksRef.current.push({\n                x: containerWidthRef.current,\n                y: 0,\n                angle: angle,\n                radius: ringIndex * ringSpacing,\n                startTime: timestamp + ringDelay,\n                waveId: currentWaveId + 1,\n                ringIndex: ringIndex,\n                direction: \"outward\",\n                corner: \"right\",\n              });\n            }\n          }\n        }\n\n        waveIdRef.current += 2;\n        lastWaveTimeRef.current = timestamp;\n      }\n\n      if (\n        enableInward &&\n        timestamp - lastInwardWaveTimeRef.current >= waveInterval\n      ) {\n        const currentWaveId = waveIdRef.current++;\n        const maxScreenRadius =\n          Math.sqrt(\n            containerWidthRef.current * containerWidthRef.current +\n              containerHeightRef.current * containerHeightRef.current\n          ) /\n            2 +\n          100;\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            if (corners === \"both\" || corners === \"left\") {\n              sparksRef.current.push({\n                x: 0,\n                y: 0,\n                angle: angle,\n                radius: maxScreenRadius - ringIndex * ringSpacing,\n                startTime: timestamp + ringDelay,\n                waveId: currentWaveId,\n                ringIndex: ringIndex,\n                direction: \"inward\",\n                corner: \"left\",\n              });\n            }\n\n            if (corners === \"both\" || corners === \"right\") {\n              sparksRef.current.push({\n                x: containerWidthRef.current,\n                y: 0,\n                angle: angle + Math.PI,\n                radius: maxScreenRadius - ringIndex * ringSpacing,\n                startTime: timestamp + ringDelay,\n                waveId: currentWaveId + 1,\n                ringIndex: ringIndex,\n                direction: \"inward\",\n                corner: \"right\",\n              });\n            }\n          }\n        }\n\n        waveIdRef.current += 2;\n        lastInwardWaveTimeRef.current = timestamp;\n      }\n\n      sparksRef.current = sparksRef.current.filter((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 maxScreenRadius =\n            Math.sqrt(\n              containerWidthRef.current * containerWidthRef.current +\n                containerHeightRef.current * containerHeightRef.current\n            ) /\n              2 +\n            100;\n          const contractionDistance = eased * (maxScreenRadius - 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        const shadowBlur = isDarkMode() ? 15 : 8;\n        const lineWidth = isDarkMode() ? 2.5 : 2;\n\n        ctx.shadowBlur = shadowBlur;\n        ctx.shadowColor = currentSparkColor;\n        ctx.strokeStyle = currentSparkColor;\n        ctx.globalAlpha = opacity;\n        ctx.lineWidth = lineWidth;\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\n      animationIdRef.current = requestAnimationFrame(animate);\n    };\n\n    animationIdRef.current = requestAnimationFrame(animate);\n\n    const observer = new MutationObserver(() => {\n      sparksRef.current = [];\n      waveIdRef.current = 0;\n      lastWaveTimeRef.current = 0;\n      lastInwardWaveTimeRef.current = 700;\n    });\n\n    if (typeof document !== \"undefined\") {\n      observer.observe(document.documentElement, {\n        attributes: true,\n        attributeFilter: [\"class\"],\n      });\n    }\n\n    return () => {\n      cancelAnimationFrame(animationIdRef.current);\n      observer.disconnect();\n    };\n  }, [\n    sparkColor,\n    sparkColorDark,\n    sparkSize,\n    sparkCount,\n    duration,\n    waveInterval,\n    maxRadius,\n    ringsPerWave,\n    ringSpacing,\n    enableInward,\n    corners,\n    backgroundColor,\n    backgroundColorDark,\n    clearBackground,\n    backgroundOpacity,\n    easeFunc,\n    getSparkColor,\n    getBackgroundColor,\n    isDarkMode,\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}"
    }
  ]
}
