{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confetti",
  "title": "Confetti",
  "description": "Deterministic confetti burst for Remotion — seeded particles with gravity, spin, and flutter so every render is identical. Fire it on any frame as a celebratory overlay.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/confetti/index.tsx",
      "content": "\"use client\";\n\nimport {\n  AbsoluteFill,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport interface ConfettiProps {\n  /** Number of confetti pieces. */\n  particleCount?: number;\n  colors?: string[];\n  /** Burst origin as a 0..1 fraction of the canvas. */\n  originX?: number;\n  originY?: number;\n  /** Frame the burst fires on. */\n  startFrame?: number;\n  /** Lifetime of the burst, in frames. */\n  lifetime?: number;\n  /** Initial launch speed (px/frame at the 720px reference height). */\n  power?: number;\n  /** Downward acceleration (px/frame² at the 720px reference height). */\n  gravity?: number;\n  /** Base piece size in reference px. */\n  size?: number;\n  /** PRNG seed — same seed renders an identical burst every time. */\n  seed?: number;\n}\n\nexport interface Particle {\n  angle: number;\n  speed: number;\n  color: string;\n  size: number;\n  rot0: number;\n  spin: number;\n  drift: number;\n}\n\nconst DEFAULT_COLORS = [\n  \"#1d9bf0\",\n  \"#ff5da2\",\n  \"#ffd23f\",\n  \"#22c55e\",\n  \"#a855f7\",\n  \"#ff7a45\",\n];\n\n// --- Pure helpers (unit-tested) -------------------------------------------\n\n/** Deterministic PRNG (mulberry32) — returns a function yielding [0, 1). */\nexport function mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n/** Build a deterministic particle set for a seed (stable across renders). */\nexport function makeParticles({\n  count,\n  seed,\n  colors,\n}: {\n  count: number;\n  seed: number;\n  colors: string[];\n}): Particle[] {\n  const rand = mulberry32(seed);\n  const palette = colors.length > 0 ? colors : DEFAULT_COLORS;\n  const out: Particle[] = [];\n  for (let i = 0; i < count; i++) {\n    const angle = rand() * Math.PI * 2;\n    const speed = 0.35 + 0.65 * rand();\n    out.push({\n      angle,\n      speed,\n      color: palette[Math.floor(rand() * palette.length)],\n      size: 0.7 + 0.6 * rand(),\n      rot0: rand() * Math.PI * 2,\n      spin: (rand() - 0.5) * 0.6,\n      drift: (rand() - 0.5) * 2,\n    });\n  }\n  return out;\n}\n\n/** Opacity envelope over a particle's lifetime: quick fade-in, slow fade-out. */\nexport function particleOpacity(tt: number, lifetime: number): number {\n  if (tt < 0 || tt > lifetime) return 0;\n  const fadeIn = interpolate(tt, [0, 4], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const fadeOut = interpolate(tt, [lifetime * 0.7, lifetime], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  return Math.min(fadeIn, fadeOut);\n}\n\n// --- Component -------------------------------------------------------------\n\nexport function Confetti({\n  particleCount = 140,\n  colors = DEFAULT_COLORS,\n  originX = 0.5,\n  originY = 0.5,\n  startFrame = 0,\n  lifetime = 90,\n  power = 17,\n  gravity = 0.45,\n  size = 13,\n  seed = 1,\n}: ConfettiProps) {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n\n  const tt = frame - startFrame;\n  if (tt < 0 || tt > lifetime) return null;\n\n  const sc = height / 720;\n  const ox = width * originX;\n  const oy = height * originY;\n  const particles = makeParticles({ count: particleCount, seed, colors });\n\n  return (\n    <AbsoluteFill style={{ pointerEvents: \"none\" }}>\n      {particles.map((p, i) => {\n        const opacity = particleOpacity(tt, lifetime);\n        if (opacity <= 0) return null;\n\n        const vx = Math.cos(p.angle) * p.speed * power + p.drift;\n        const vy = Math.sin(p.angle) * p.speed * power;\n        const x = ox + vx * tt * sc;\n        const y = oy + (vy * tt + 0.5 * gravity * tt * tt) * sc;\n        const rot = p.rot0 + p.spin * tt;\n        const flutter = Math.cos(p.rot0 + p.spin * tt * 1.6);\n        const w = size * p.size * sc;\n\n        return (\n          <div\n            key={i}\n            style={{\n              position: \"absolute\",\n              left: x,\n              top: y,\n              width: w,\n              height: w * 0.62,\n              borderRadius: 2,\n              background: p.color,\n              opacity,\n              transform: `translate(-50%, -50%) rotate(${rot}rad) scaleX(${flutter})`,\n            }}\n          />\n        );\n      })}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/confetti.tsx"
    }
  ],
  "type": "registry:component"
}
