{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confetti",
  "type": "registry:ui",
  "title": "Confetti",
  "description": "CSS-only confetti burst with randomized colors and trajectories — celebrate conversions and milestones.",
  "dependencies": [
    "@saasflare/ui"
  ],
  "files": [
    {
      "path": "components/ui/confetti.tsx",
      "type": "registry:ui",
      "content": "\"use client\"\n\n/**\n * @fileoverview Confetti burst animation trigger.\n * @author Saasflare™\n * CSS-only confetti burst with randomized particle colors, sizes, and\n * trajectories. No canvas, no external dependencies.\n * @module packages/ui/components/ui/confetti\n * @package ui\n *\n * @component\n * @example\n * import { Confetti } from '@saasflare/ui';\n * const [show, setShow] = useState(false);\n * <button onClick={() => setShow(true)}>Celebrate!</button>\n * <Confetti active={show} onComplete={() => setShow(false)} />\n *\n * @example\n * // Custom colors and particle count\n * <Confetti active={active} count={60} colors={[\"#ff6b6b\", \"#ffd93d\", \"#6bcb77\"]} />\n */\n\nimport { useEffect, useMemo, useRef, useState } from \"react\"\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\nimport { useReducedMotion } from \"@saasflare/ui\"\n\n/** Default particle palette — module-level so its identity is stable across renders. */\nconst DEFAULT_CONFETTI_COLORS: string[] = [\n  \"var(--primary)\",\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n]\n\n/** Props for the Confetti component. */\nexport interface ConfettiProps extends SaasflareComponentProps {\n  /** Whether the confetti burst is active. */\n  active: boolean\n  /** Number of confetti particles. Default: `40` */\n  count?: number\n  /** Array of CSS colors for particles. */\n  colors?: string[]\n  /** Duration in milliseconds before auto-cleanup. Default: `3000` */\n  duration?: number\n  /** Callback when animation completes. */\n  onComplete?: () => void\n  /** Additional class names. */\n  className?: string\n}\n\n/** Deterministic pseudo-random for consistent particles. */\nfunction seeded(seed: number): number {\n  const x = Math.sin(seed * 9301 + 49297) * 233280\n  return x - Math.floor(x)\n}\n\n/**\n * Confetti burst animation overlay.\n *\n * - CSS-only particles with randomized trajectories\n * - Fires once when `active` becomes `true`\n * - Calls `onComplete` when the animation finishes\n * - Renders nothing when the provider's `animated` axis is off or reduced motion is preferred\n *\n * @component\n * @package ui\n */\nexport function Confetti({\n  active,\n  count = 40,\n  colors = DEFAULT_CONFETTI_COLORS,\n  duration = 3000,\n  onComplete,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  className,\n}: ConfettiProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const reduced = useReducedMotion()\n  // Animation is gated on both the provider's `animated` axis and the user's\n  // reduced-motion preference; either one suppresses the burst entirely.\n  const enabled = sf.animated && !reduced\n  const [visible, setVisible] = useState(false)\n\n  // Latest-ref: an inline `onComplete` changes identity every parent render —\n  // keying the effect on it would restart the burst mid-flight.\n  const onCompleteRef = useRef(onComplete)\n  useEffect(() => {\n    onCompleteRef.current = onComplete\n  })\n\n  useEffect(() => {\n    if (!active || !enabled) return\n    setVisible(true)\n    const timer = setTimeout(() => {\n      setVisible(false)\n      onCompleteRef.current?.()\n    }, duration)\n    return () => clearTimeout(timer)\n  }, [active, enabled, duration])\n\n  const particles = useMemo(\n    () =>\n      Array.from({ length: count }, (_, i) => {\n        const r = (s: number) => seeded(i * 100 + s)\n        return {\n          id: i,\n          color: colors[i % colors.length],\n          x: 50 + (r(1) - 0.5) * 60,\n          endX: (r(2) - 0.5) * 200,\n          endY: -(100 + r(3) * 300),\n          size: 4 + r(4) * 6,\n          rotation: r(5) * 720,\n          delay: r(6) * 0.3,\n          animDuration: 1.5 + r(7) * 1.5,\n        }\n      }),\n    [count, colors],\n  )\n\n  if (!visible || !enabled) return null\n\n  return (\n    <div\n      className={cn(\"pointer-events-none fixed inset-0 z-[9999] overflow-hidden\", className)}\n      aria-hidden=\"true\"\n      data-slot=\"confetti\"\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n    >\n      <style>{`\n        @keyframes sf-confetti-burst {\n          0% {\n            transform: translate(0, 0) rotate(0deg);\n            opacity: 1;\n          }\n          100% {\n            transform: translate(var(--cx), var(--cy)) rotate(var(--cr));\n            opacity: 0;\n          }\n        }\n      `}</style>\n      {particles.map((p) => (\n        <div\n          key={p.id}\n          style={{\n            position: \"absolute\",\n            left: `${p.x}%`,\n            bottom: 0,\n            width: p.size,\n            height: p.size * 0.6,\n            backgroundColor: p.color,\n            borderRadius: p.id % 3 === 0 ? \"50%\" : \"1px\",\n            [\"--cx\" as string]: `${p.endX}px`,\n            [\"--cy\" as string]: `${p.endY}px`,\n            [\"--cr\" as string]: `${p.rotation}deg`,\n            animation: `sf-confetti-burst ${p.animDuration}s cubic-bezier(0.25, 0.46, 0.45, 0.94) ${p.delay}s forwards`,\n          }}\n        />\n      ))}\n    </div>\n  )\n}\n",
      "target": "components/ui/confetti.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/confetti.json"
  }
}
