{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wavy-tide",
  "title": "Wavy Tide",
  "description": "Animated canvas waves that fill the container and drift with the pointer - a softer, SaaS-ready take on classic wavy backgrounds.",
  "files": [
    {
      "path": "components/backgrounds/WavyTideBackground.tsx",
      "content": "'use client';\n\nimport React, { useEffect, useRef, type ReactNode } from 'react';\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n//\n// children           Content layered above the canvas (headline, CTA, etc.)\n// className          Styles for the content wrapper\n// containerClassName Styles for the outer shell (size, layout)\n// colors             Wave layer colors (hex array); falls back to a warm palette\n// backgroundFill     Solid color behind the waves (hex)\n// waveWidth          Stroke width of each wave line\n// speed              \"slow\" or \"fast\" (default) — how quickly waves drift\n// waveOpacity        Overall opacity of the wave layers (0–1)\n//\ntype WavyTideBackgroundProps = {\n  children?: ReactNode;\n  className?: string;\n  containerClassName?: string;\n  colors?: string[];\n  backgroundFill?: string;\n  waveWidth?: number;\n  speed?: 'slow' | 'fast';\n  waveOpacity?: number;\n};\n\n// ---------------------------------------------------------------------------\n// Wave shape — layered sines + pointer pull for a soft tidal feel\n// ---------------------------------------------------------------------------\nfunction waveOffset(\n  position: number,\n  layer: number,\n  t: number,\n  pointerX: number,\n  pointerY: number\n) {\n  const pull = (pointerX - 0.5) * 36;\n  const lift = (pointerY - 0.5) * 44;\n  return (\n    Math.sin(position * 0.0035 + t + layer * 0.9) * 38 +\n    Math.sin(position * 0.008 - t * 1.4 + layer * 1.7) * 22 +\n    Math.sin(position * 0.018 + t * 0.6 + layer) * 10 +\n    Math.sin((position + pull * 8) * 0.002 + t * 0.4) * 16 +\n    lift * (0.3 + layer * 0.07)\n  );\n}\n\nexport default function WavyTideBackground({\n  children,\n  className = '',\n  containerClassName = '',\n  colors,\n  backgroundFill = '#140818',\n  waveWidth = 2,\n  speed = 'fast',\n  waveOpacity = 0.85,\n}: WavyTideBackgroundProps) {\n  // ---------------------------------------------------------------------------\n  // Refs\n  // ---------------------------------------------------------------------------\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const pointerRef = useRef({ x: 0.5, y: 0.5 });\n  const rafRef = useRef(0);\n  const timeRef = useRef(0);\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 waveColors = colors ?? [\n      '#f97316',\n      '#fb7185',\n      '#e879f9',\n      '#c084fc',\n      '#fbbf24',\n    ];\n\n    const step = speed === 'fast' ? 0.012 : 0.006;\n    const angle = -0.42;\n    let width = 0;\n    let height = 0;\n    let dpr = 1;\n    const prefersReducedMotion = window.matchMedia(\n      '(prefers-reduced-motion: reduce)'\n    ).matches;\n\n    // -------------------------------------------------------------------------\n    // Setup helpers\n    // -------------------------------------------------------------------------\n    const resize = () => {\n      const rect = container.getBoundingClientRect();\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      width = Math.max(1, Math.floor(rect.width));\n      height = Math.max(1, Math.floor(rect.height));\n      canvas.width = Math.floor(width * dpr);\n      canvas.height = Math.floor(height * dpr);\n      canvas.style.width = `${width}px`;\n      canvas.style.height = `${height}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    };\n\n    // -------------------------------------------------------------------------\n    // Draw loop — fill background, rotate canvas, stroke + fill each wave layer\n    // -------------------------------------------------------------------------\n    const draw = () => {\n      timeRef.current += prefersReducedMotion ? 0 : step;\n      const t = timeRef.current;\n      const { x: px, y: py } = pointerRef.current;\n      const span = Math.hypot(width, height) * 1.35;\n\n      ctx.globalAlpha = 1;\n      ctx.fillStyle = backgroundFill;\n      ctx.fillRect(0, 0, width, height);\n\n      ctx.save();\n      ctx.translate(width / 2, height / 2);\n      ctx.rotate(angle);\n      ctx.translate(-span / 2, -span / 2);\n      ctx.globalAlpha = waveOpacity;\n\n      for (let layer = 0; layer < 5; layer++) {\n        const baseline = span * (0.34 + layer * 0.09);\n        const color = waveColors[layer % waveColors.length];\n\n        ctx.beginPath();\n        ctx.lineWidth = waveWidth;\n        ctx.strokeStyle = color;\n        ctx.lineCap = 'round';\n        ctx.lineJoin = 'round';\n\n        for (let x = 0; x <= span; x += 4) {\n          const y = baseline + waveOffset(x, layer, t, px, py);\n          if (x === 0) ctx.moveTo(x, y);\n          else ctx.lineTo(x, y);\n        }\n        ctx.stroke();\n\n        // Fill below the wave with a soft fade\n        ctx.lineTo(span, span);\n        ctx.lineTo(0, span);\n        ctx.closePath();\n\n        const gradient = ctx.createLinearGradient(0, baseline - 60, 0, span);\n        gradient.addColorStop(0, `${color}40`);\n        gradient.addColorStop(1, 'transparent');\n        ctx.fillStyle = gradient;\n        ctx.fill();\n      }\n\n      ctx.restore();\n\n      if (!prefersReducedMotion) {\n        rafRef.current = requestAnimationFrame(draw);\n      }\n    };\n\n    // Pointer position (0–1) nudges the wave offset\n    const onPointerMove = (event: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      pointerRef.current = {\n        x: (event.clientX - rect.left) / Math.max(rect.width, 1),\n        y: (event.clientY - rect.top) / Math.max(rect.height, 1),\n      };\n    };\n\n    // -------------------------------------------------------------------------\n    // Start + cleanup\n    // -------------------------------------------------------------------------\n    resize();\n    draw();\n\n    const observer = new ResizeObserver(resize);\n    observer.observe(container);\n    container.addEventListener('pointermove', onPointerMove);\n\n    return () => {\n      cancelAnimationFrame(rafRef.current);\n      observer.disconnect();\n      container.removeEventListener('pointermove', onPointerMove);\n    };\n  }, [backgroundFill, colors, speed, waveOpacity, waveWidth]);\n\n  // ---------------------------------------------------------------------------\n  // Render — canvas behind, content on top\n  // ---------------------------------------------------------------------------\n  return (\n    <div\n      ref={containerRef}\n      className={[\n        'relative flex min-h-[420px] w-full flex-col items-center justify-center overflow-hidden',\n        containerClassName,\n      ]\n        .filter(Boolean)\n        .join(' ')}\n    >\n      <canvas\n        ref={canvasRef}\n        className='absolute inset-0 z-0 h-full w-full'\n        aria-hidden\n      />\n      <div\n        className={['relative z-10 px-6 text-center', className]\n          .filter(Boolean)\n          .join(' ')}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/backgrounds/WavyTideBackground.tsx"
    }
  ],
  "categories": [
    "backgrounds"
  ],
  "type": "registry:component"
}