{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-wave",
  "type": "registry:ui",
  "title": "Dot Wave",
  "description": "Dot-based waves ripple outward in symmetry, creating a subtle, motion-driven background layer",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/dot-wave.tsx",
      "content": "'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { cn } from '@/lib/utils';\n\ninterface DotWaveProps {\n  dotGap?: number;\n  sphereRadius?: number;\n  dotRadiusMax?: number;\n  speed?: number;\n  expansionSpeed?: number;\n  repeatAnimation?: boolean;\n  lightIntensity?: number;\n  fadeIntensity?: number;\n  followMouse?: boolean;\n  className?: string;\n  dotClassName?: string;\n  children?: React.ReactNode;\n  bgColor?: string;\n  dotColor?: string;\n}\n\nclass Ease {\n  value: number;\n  begin: number;\n  end: number;\n  pow: number;\n  maxDuration: number;\n  time: number;\n  duration: number;\n\n  constructor(value: number, pow: number, duration: number, timeBegin: number) {\n    this.value = this.begin = this.end = value;\n    this.pow = pow;\n    this.maxDuration = duration;\n    this.time = timeBegin;\n    this.duration = 0;\n    this.init();\n  }\n\n  init() {\n    this.begin = this.end;\n    this.end = Math.random();\n    this.time = 0;\n    this.duration =\n      Math.sqrt(Math.abs(this.end - this.begin)) * this.maxDuration;\n  }\n\n  update(timeChange = 1) {\n    let timeRatio = this.time / this.duration;\n\n    if (timeRatio < 0.5) {\n      timeRatio = 0.5 * Math.pow(timeRatio * 2, this.pow);\n    } else {\n      timeRatio = 1 - 0.5 * Math.pow((1 - timeRatio) * 2, this.pow);\n    }\n\n    this.value = this.begin + timeRatio * (this.end - this.begin);\n    this.time += timeChange;\n    if (this.time > this.duration) {\n      this.init();\n    }\n  }\n}\n\nexport function DotWave({\n  dotGap = 20,\n  sphereRadius = 200,\n  dotRadiusMax = 3,\n  speed = 0.15,\n  expansionSpeed = 150,\n  repeatAnimation = true,\n  lightIntensity = 0.5,\n  fadeIntensity = 0.15,\n  followMouse = false,\n  className,\n  dotClassName,\n  children,\n  bgColor = '#000000',\n  dotColor = '#ffffff',\n}: DotWaveProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const stateRef = useRef({\n    canvas: null as HTMLCanvasElement | null,\n    ctx: null as CanvasRenderingContext2D | null,\n    center: { x: 0, y: 0 },\n    windowSize: { w: 0, h: 0 },\n    circleNumber: { x: 0, y: 0 },\n    posStart: { x: 0, y: 0 },\n    easeX: new Ease(0.5, 2, 60, 0),\n    easeY: new Ease(0.5, 2, 60, 0),\n    animationId: 0,\n    expansionRadius: 0,\n    startTime: 0,\n    bgColor: '#000000',\n    dotColor: '#ffffff',\n  });\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return;\n\n    const state = stateRef.current;\n    state.canvas = canvas;\n    state.ctx = ctx;\n    state.startTime = Date.now();\n    state.expansionRadius = 0;\n\n    const updateColors = () => {\n      const computedStyle = getComputedStyle(container);\n      const containerBg = computedStyle.backgroundColor;\n      const containerColor = computedStyle.color;\n\n      state.bgColor =\n        containerBg &&\n        containerBg !== 'rgba(0, 0, 0, 0)' &&\n        containerBg !== 'transparent'\n          ? containerBg\n          : bgColor;\n\n      state.dotColor =\n        containerColor &&\n        containerColor !== 'rgba(0, 0, 0, 0)' &&\n        containerColor !== 'transparent'\n          ? containerColor\n          : dotColor;\n    };\n\n    updateColors();\n\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n    const handleThemeChange = () => updateColors();\n    mediaQuery.addEventListener('change', handleThemeChange);\n\n    const handleResize = () => {\n      state.windowSize = {\n        w: window.innerWidth,\n        h: window.innerHeight,\n      };\n\n      canvas.width = state.windowSize.w;\n      canvas.height = state.windowSize.h;\n\n      state.center = {\n        x: state.windowSize.w / 2,\n        y: state.windowSize.h / 2,\n      };\n\n      state.startTime = Date.now();\n      state.expansionRadius = 0;\n\n      setDotParams();\n    };\n\n    const setDotParams = () => {\n      state.circleNumber = {\n        x: Math.floor(state.windowSize.w / dotGap) + 2,\n        y: Math.floor(state.windowSize.h / dotGap) + 1,\n      };\n\n      state.posStart = {\n        x: Math.round(\n          (state.windowSize.w - (state.circleNumber.x - 1) * dotGap) / 2,\n        ),\n        y: Math.round(\n          (state.windowSize.h - (state.circleNumber.y - 1) * dotGap) / 2,\n        ),\n      };\n    };\n\n    const getDistance = (x: number, y: number) => {\n      const distanceX = x - state.center.x;\n      const distanceY = y - state.center.y;\n      return Math.sqrt(distanceX * distanceX + distanceY * distanceY);\n    };\n\n    const drawDots = () => {\n      ctx.beginPath();\n      ctx.fillStyle = state.bgColor;\n      ctx.rect(0, 0, state.windowSize.w, state.windowSize.h);\n      ctx.fill();\n      ctx.closePath();\n\n      const elapsed = Date.now() - state.startTime;\n      state.expansionRadius = (elapsed / 1000) * expansionSpeed;\n\n      const maxScreenRadius =\n        Math.sqrt(\n          state.windowSize.w * state.windowSize.w +\n            state.windowSize.h * state.windowSize.h,\n        ) / 2;\n\n      if (repeatAnimation && state.expansionRadius > maxScreenRadius + 100) {\n        state.startTime = Date.now();\n        state.expansionRadius = 0;\n      }\n\n      for (let i = 0; i < state.circleNumber.x; i++) {\n        for (let j = 0; j < state.circleNumber.y; j++) {\n          const gapX = j % 2 === 0 ? -dotGap / 2 : 0;\n          const x = state.posStart.x + gapX + i * dotGap;\n          const y = state.posStart.y + j * dotGap;\n\n          const distance = getDistance(x, y);\n\n          if (distance <= maxScreenRadius) {\n            const baseAlpha = lightIntensity;\n            const radius = dotRadiusMax;\n\n            const waveFrontWidth = 80;\n            const distanceFromWave = Math.abs(distance - state.expansionRadius);\n            const fadeOutDistance = 150;\n\n            let dotAlpha = 0;\n            if (distance <= state.expansionRadius) {\n              const behindWave = state.expansionRadius - distance;\n\n              if (distanceFromWave < waveFrontWidth) {\n                const glowIntensity = 1 - distanceFromWave / waveFrontWidth;\n                dotAlpha = Math.min(baseAlpha + glowIntensity * 0.6, 1);\n              } else if (behindWave < fadeOutDistance) {\n                const fadeProgress = behindWave / fadeOutDistance;\n                dotAlpha = baseAlpha * (1 - fadeProgress * (1 - fadeIntensity));\n              } else {\n                dotAlpha = baseAlpha * fadeIntensity;\n              }\n            }\n\n            if (dotAlpha > 0) {\n              ctx.save();\n              ctx.globalAlpha = Math.min(dotAlpha, 1);\n\n              if (\n                distanceFromWave < waveFrontWidth &&\n                distance <= state.expansionRadius\n              ) {\n                const glowIntensity = 1 - distanceFromWave / waveFrontWidth;\n                ctx.shadowBlur = 15 * glowIntensity;\n                ctx.shadowColor = state.dotColor;\n              }\n\n              ctx.beginPath();\n              ctx.fillStyle = state.dotColor;\n              ctx.arc(x, y, radius, 0, 2 * Math.PI, false);\n              ctx.fill();\n              ctx.closePath();\n              ctx.restore();\n            }\n          }\n        }\n      }\n    };\n\n    const moveCenter = (e: MouseEvent | null) => {\n      if (e === null) {\n        state.easeX.update(speed);\n        state.easeY.update(speed);\n        state.center.x = state.easeX.value * state.windowSize.w;\n        state.center.y = state.easeY.value * state.windowSize.h;\n      } else {\n        state.center.x = e.pageX;\n        state.center.y = e.pageY;\n      }\n    };\n\n    const render = () => {\n      drawDots();\n    };\n\n    const draw = () => {\n      state.animationId = requestAnimationFrame(draw);\n      render();\n    };\n\n    const handleMouseMove = (e: MouseEvent) => {\n      if (followMouse) {\n        moveCenter(e);\n      }\n    };\n\n    handleResize();\n    window.addEventListener('resize', handleResize);\n    canvas.addEventListener('mousemove', handleMouseMove);\n\n    draw();\n\n    return () => {\n      window.removeEventListener('resize', handleResize);\n      canvas.removeEventListener('mousemove', handleMouseMove);\n      mediaQuery.removeEventListener('change', handleThemeChange);\n      cancelAnimationFrame(state.animationId);\n    };\n  }, [\n    dotGap,\n    sphereRadius,\n    dotRadiusMax,\n    speed,\n    expansionSpeed,\n    repeatAnimation,\n    lightIntensity,\n    fadeIntensity,\n    followMouse,\n    bgColor,\n    dotColor,\n  ]);\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn('relative overflow-hidden', className)}\n      style={\n        {\n          '--dot-wave-bg': 'var(--dot-wave-bg, light-dark(#ffffff, #0a0a0a))',\n          '--dot-wave-color':\n            'var(--dot-wave-color, light-dark(#000000, #00d4ff))',\n        } as React.CSSProperties\n      }\n    >\n      <canvas\n        ref={canvasRef}\n        className={cn('absolute inset-0 w-full h-full', dotClassName)}\n      />\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}"
    }
  ]
}
