{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "meteor-shower",
  "title": "Meteor Shower",
  "description": "Subtle canvas meteors drifting across a dark sky - a quiet animated background for heroes and landing sections.",
  "files": [
    {
      "path": "components/backgrounds/MeteorShowerBackground.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// backgroundFill     Solid sky color behind the meteors (hex)\n// meteorColor        Streak color (hex)\n// meteorCount        How many meteors run at once\n// speed              \"slow\" (default, subtle) or \"fast\"\n//\ntype MeteorShowerBackgroundProps = {\n  children?: ReactNode;\n  className?: string;\n  containerClassName?: string;\n  backgroundFill?: string;\n  meteorColor?: string;\n  meteorCount?: number;\n  speed?: 'slow' | 'fast';\n};\n\n// ---------------------------------------------------------------------------\n// Meteor state\n// ---------------------------------------------------------------------------\ntype Meteor = {\n  x: number;\n  y: number;\n  len: number;\n  speed: number;\n  thickness: number;\n  life: number;\n  maxLife: number;\n  angle: number;\n  delay: number;\n};\n\n// ---------------------------------------------------------------------------\n// Spawn a single meteor just off-screen\n// ---------------------------------------------------------------------------\nfunction spawnMeteor(width: number, height: number, fast: boolean): Meteor {\n  const fromTop = Math.random() > 0.35;\n  return {\n    x: fromTop ? Math.random() * width * 1.2 : -40,\n    y: fromTop ? -40 : Math.random() * height * 0.55,\n    len: 100 + Math.random() * 120,\n    speed: (fast ? 2.4 : 1.1) + Math.random() * (fast ? 2.2 : 0.9),\n    thickness: 0.9 + Math.random() * 1.2,\n    life: 0,\n    maxLife: 1.2 + Math.random() * 1.4,\n    angle: Math.PI / 4 + (Math.random() - 0.5) * 0.18,\n    delay: Math.random() * 4,\n  };\n}\n\nexport default function MeteorShowerBackground({\n  children,\n  className = '',\n  containerClassName = '',\n  backgroundFill = '#070b16',\n  meteorColor = '#e2e8f0',\n  meteorCount = 9,\n  speed = 'slow',\n}: MeteorShowerBackgroundProps) {\n  // ---------------------------------------------------------------------------\n  // Refs\n  // ---------------------------------------------------------------------------\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const rafRef = useRef(0);\n  const meteorsRef = useRef<Meteor[]>([]);\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 fast = speed === 'fast';\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 seedMeteors = () => {\n      meteorsRef.current = Array.from({ length: meteorCount }, () =>\n        spawnMeteor(width, height, fast)\n      );\n    };\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      if (!meteorsRef.current.length) seedMeteors();\n    };\n\n    // -------------------------------------------------------------------------\n    // Draw loop — fill sky, move + fade each meteor, respawn when done\n    // -------------------------------------------------------------------------\n    const draw = () => {\n      ctx.globalAlpha = 1;\n      ctx.fillStyle = backgroundFill;\n      ctx.fillRect(0, 0, width, height);\n\n      for (let i = 0; i < meteorsRef.current.length; i++) {\n        const m = meteorsRef.current[i];\n\n        // Wait before first appearance so meteors don't all start together\n        if (!prefersReducedMotion) {\n          if (m.delay > 0) {\n            m.delay -= fast ? 0.016 : 0.01;\n            continue;\n          }\n          m.x += Math.cos(m.angle) * m.speed;\n          m.y += Math.sin(m.angle) * m.speed;\n          m.life += fast ? 0.008 : 0.004;\n        }\n\n        // Soft fade in / fade out over the meteor's life\n        const progress = m.life / m.maxLife;\n        const fade =\n          progress < 0.2\n            ? progress / 0.2\n            : progress > 0.65\n              ? Math.max(0, 1 - (progress - 0.65) / 0.35)\n              : 1;\n\n        const tailX = m.x - Math.cos(m.angle) * m.len;\n        const tailY = m.y - Math.sin(m.angle) * m.len;\n        const gradient = ctx.createLinearGradient(tailX, tailY, m.x, m.y);\n        gradient.addColorStop(0, 'transparent');\n        gradient.addColorStop(0.6, `${meteorColor}33`);\n        gradient.addColorStop(1, `${meteorColor}aa`);\n\n        ctx.beginPath();\n        ctx.strokeStyle = gradient;\n        ctx.lineWidth = m.thickness;\n        ctx.lineCap = 'round';\n        ctx.globalAlpha = fade * 0.55;\n        ctx.moveTo(tailX, tailY);\n        ctx.lineTo(m.x, m.y);\n        ctx.stroke();\n\n        // Respawn once it leaves the view or finishes fading\n        const offscreen =\n          m.x > width + 80 || m.y > height + 80 || progress >= 1;\n        if (offscreen && !prefersReducedMotion) {\n          meteorsRef.current[i] = spawnMeteor(width, height, fast);\n        }\n      }\n\n      ctx.globalAlpha = 1;\n\n      if (!prefersReducedMotion) {\n        rafRef.current = requestAnimationFrame(draw);\n      }\n    };\n\n    // -------------------------------------------------------------------------\n    // Start + cleanup\n    // -------------------------------------------------------------------------\n    resize();\n    seedMeteors();\n    draw();\n\n    const observer = new ResizeObserver(resize);\n    observer.observe(container);\n\n    return () => {\n      cancelAnimationFrame(rafRef.current);\n      observer.disconnect();\n    };\n  }, [backgroundFill, meteorColor, meteorCount, speed]);\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/MeteorShowerBackground.tsx"
    }
  ],
  "categories": [
    "backgrounds"
  ],
  "type": "registry:component"
}