{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shooting-stars",
  "title": "Shooting Stars",
  "description": "A twinkling star field with brand-tinted streaks shooting across at random.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/shooting-stars.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype Draw = (t: number, dt: number, still: boolean) => void;\ntype Build = (ctx: CanvasRenderingContext2D, w: number, h: number, el: HTMLCanvasElement) => Draw;\n\n// CSS colour or \"--token\" → \"r, g, b\", so oklch() tokens work in gradients.\nfunction rgb(el: Element, color: string) {\n  const c = document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true })!;\n  c.fillStyle = color.startsWith(\"--\") ? getComputedStyle(el).getPropertyValue(color) : color;\n  c.fillRect(0, 0, 1, 1);\n  const [r, g, b] = c.getImageData(0, 0, 1, 1).data;\n  return `${r}, ${g}, ${b}`;\n}\n\n// Rebuilds on resize/theme change. Loops only while visible and motion is\n// allowed; otherwise paints one still frame.\nfunction useCanvasLoop(ref: React.RefObject<HTMLCanvasElement | null>, build: Build) {\n  useEffect(() => {\n    const el = ref.current;\n    const ctx = el?.getContext(\"2d\");\n    if (!el || !ctx) return;\n    const mq = matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let draw: Draw | undefined;\n    let raf = 0;\n    let t = 0;\n    let last = 0;\n    let seen = false;\n    const frame = (now: number) => {\n      const dt = Math.max(0, Math.min(now - last, 50));\n      last = now;\n      t += dt;\n      draw?.(t, dt, false);\n      raf = requestAnimationFrame(frame);\n    };\n    const sync = () => {\n      cancelAnimationFrame(raf);\n      if (!draw) return;\n      if (mq.matches) draw(t, 0, true);\n      else if (seen && !document.hidden) {\n        last = performance.now();\n        raf = requestAnimationFrame(frame);\n      }\n    };\n    const setup = () => {\n      const dpr = Math.min(devicePixelRatio || 1, 2);\n      const w = el.clientWidth;\n      const h = el.clientHeight;\n      el.width = w * dpr;\n      el.height = h * dpr;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      draw = w && h ? build(ctx, w, h, el) : undefined;\n      sync();\n    };\n    const ro = new ResizeObserver(setup);\n    const io = new IntersectionObserver(([e]) => {\n      seen = e.isIntersecting;\n      sync();\n    });\n    const mo = new MutationObserver(setup);\n    ro.observe(el);\n    io.observe(el);\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"style\"] });\n    mo.observe(document.head, { childList: true, subtree: true, characterData: true });\n    document.addEventListener(\"visibilitychange\", sync);\n    mq.addEventListener(\"change\", sync);\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", sync);\n      mq.removeEventListener(\"change\", sync);\n    };\n  }, [ref, build]);\n}\n\n// Stable pseudo-random 0–1: stars keep their places across rebuilds.\nconst seed = (i: number, k: number) => {\n  const s = Math.sin(i * 12.9898 + k * 78.233) * 43758.5453;\n  return s - Math.floor(s);\n};\n\ninterface StarsBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Stars per 10,000 px² */\n  density?: number;\n}\n\n/**\n * A still field of stars that gently twinkle. Stars take the inherited text\n * colour (set `text-white` on a dark section); one in five is brand-tinted.\n * Absolutely positioned — stack `ShootingStars` on top for the full sky.\n */\nexport function StarsBackground({ density = 5, className, ...props }: StarsBackgroundProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n\n  const build = useCallback<Build>(\n    (ctx, w, h, el) => {\n      const fg = `rgb(${rgb(el, getComputedStyle(el).color)})`;\n      const brand = `rgb(${rgb(el, \"--brand-via\")})`;\n      const stars = Array.from({ length: ((w * h) / 1e4) * density }, (_, i) => ({\n        x: seed(i, 1) * w,\n        y: seed(i, 2) * h,\n        r: 0.4 + seed(i, 3) ** 3 * 1.4,\n        c: seed(i, 4) < 0.2 ? brand : fg,\n        a: 0.3 + seed(i, 5) * 0.6,\n        // Two in three twinkle\n        s: seed(i, 6) < 0.65 ? 0.0006 + seed(i, 7) * 0.002 : 0,\n        p: seed(i, 8) * 7,\n      }));\n      return (t) => {\n        ctx.clearRect(0, 0, w, h);\n        for (const s of stars) {\n          ctx.globalAlpha = s.a * (s.s ? 0.6 + 0.4 * Math.sin(t * s.s + s.p) : 1);\n          ctx.fillStyle = s.c;\n          ctx.beginPath();\n          ctx.arc(s.x, s.y, s.r, 0, 7);\n          ctx.fill();\n        }\n      };\n    },\n    [density]\n  );\n  useCanvasLoop(ref, build);\n\n  return (\n    <div\n      aria-hidden\n      data-slot=\"stars-background\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      {...props}\n    >\n      <canvas ref={ref} className=\"absolute inset-0 size-full\" />\n    </div>\n  );\n}\n\ninterface ShootingStarsProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Average pause between streaks in ms; each pause varies ±60% */\n  delay?: number;\n  /** Travel speed in px per second; each streak varies ±30% */\n  speed?: number;\n  /** Direction in degrees below horizontal; each streak varies ±12° */\n  angle?: number;\n  /** Trail length in px */\n  length?: number;\n  /** Any CSS colour or custom property name */\n  color?: string;\n}\n\n/**\n * Occasional brand-tinted streaks crossing the section at a random angle,\n * speed and delay. Absolutely positioned — layer over `StarsBackground`.\n * Under reduced motion a single streak is shown frozen mid-flight.\n */\nexport function ShootingStars({\n  delay = 2400,\n  speed = 900,\n  angle = 25,\n  length = 150,\n  color = \"--brand-to\",\n  className,\n  ...props\n}: ShootingStarsProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n\n  const build = useCallback<Build>(\n    (ctx, w, h, el) => {\n      const c = rgb(el, color);\n      const rad = Math.PI / 180;\n      // [x, y, dirX, dirY, px per ms, distance]\n      const streaks: number[][] = [];\n      let next = 0;\n      // Draws a streak; returns true once its tail has left the canvas.\n      const paint = ([x0, y0, dx, dy, , d]: number[]) => {\n        const l = Math.min(length, d);\n        const x = x0 + dx * d;\n        const y = y0 + dy * d;\n        const tx = x - dx * l;\n        const ty = y - dy * l;\n        const g = ctx.createLinearGradient(tx, ty, x, y);\n        g.addColorStop(0, `rgba(${c}, 0)`);\n        g.addColorStop(1, `rgb(${c})`);\n        ctx.strokeStyle = g;\n        ctx.beginPath();\n        ctx.moveTo(tx, ty);\n        ctx.lineTo(x, y);\n        ctx.stroke();\n        ctx.fillStyle = ctx.shadowColor = `rgb(${c})`;\n        ctx.shadowBlur = 10;\n        ctx.beginPath();\n        ctx.arc(x, y, 1.6, 0, 7);\n        ctx.fill();\n        ctx.shadowBlur = 0;\n        return tx > w || ty > h;\n      };\n      ctx.lineWidth = 1.5;\n      ctx.lineCap = \"round\";\n      return (t, dt, still) => {\n        ctx.clearRect(0, 0, w, h);\n        if (still) {\n          paint([w * 0.08, h * 0.1, Math.cos(angle * rad), Math.sin(angle * rad), 0, length * 1.5]);\n          return;\n        }\n        if (t >= next) {\n          const a = (angle + Math.random() * 24 - 12) * rad;\n          const top = Math.random() < w / (w + h);\n          streaks.push([\n            top ? Math.random() * w * 0.8 : 0,\n            top ? 0 : Math.random() * h * 0.6,\n            Math.cos(a),\n            Math.sin(a),\n            (speed / 1000) * (0.7 + Math.random() * 0.6),\n            0,\n          ]);\n          next = t + delay * (0.4 + Math.random() * 1.2);\n        }\n        for (let i = streaks.length; i--; ) {\n          streaks[i][5] += streaks[i][4] * dt;\n          if (paint(streaks[i])) streaks.splice(i, 1);\n        }\n      };\n    },\n    [delay, speed, angle, length, color]\n  );\n  useCanvasLoop(ref, build);\n\n  return (\n    <div\n      aria-hidden\n      data-slot=\"shooting-stars\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      {...props}\n    >\n      <canvas ref={ref} className=\"absolute inset-0 size-full\" />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/shooting-stars.tsx"
    }
  ],
  "type": "registry:ui"
}