{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confetti-burst",
  "type": "registry:component",
  "title": "Confetti Burst",
  "description": "Confetti that blooms out of the button you pressed, hangs at the top of its arc in the air drag, flutters as each piece turns over and drifts down; reduced motion gets a quick check instead.",
  "categories": [
    "motion",
    "Celebration"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/confetti-burst.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport const confettiPalettes = {\n  party: [\"bg-rose-500\", \"bg-amber-400\", \"bg-emerald-500\", \"bg-sky-500\", \"bg-violet-500\"],\n  brand: [\"bg-brand\", \"bg-brand-ink\", \"bg-foreground\", \"bg-emerald-500\"],\n  mono: [\"bg-foreground\", \"bg-muted-foreground\", \"bg-neutral-400\"],\n} as const;\n\nexport type ConfettiPalette = keyof typeof confettiPalettes | string[];\n\nconst W = 760;\nconst H = 680;\nconst OY = 320;\nconst EDGE = 56;\n\ntype Shape = \"rect\" | \"strip\" | \"dot\";\ninterface Bit {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  rot: number;\n  spin: number;\n  flip: number;\n  flipSpeed: number;\n  sway: number;\n  w: number;\n  h: number;\n  shape: Shape;\n  color: string;\n  age: number;\n  life: number;\n}\n\nconst rand = (a: number, b: number) => a + Math.random() * (b - a);\n\nfunction makeBits(count: number, colors: string[], ox: number): Bit[] {\n  return Array.from({ length: count }, () => {\n    const angle = (-90 + rand(-52, 52)) * (Math.PI / 180);\n    const speed = rand(480, 1050);\n    const shape: Shape = Math.random() < 0.55 ? \"rect\" : Math.random() < 0.6 ? \"strip\" : \"dot\";\n    return {\n      x: ox + rand(-8, 8),\n      y: OY + rand(-4, 4),\n      vx: Math.cos(angle) * speed,\n      vy: Math.sin(angle) * speed,\n      rot: rand(0, Math.PI * 2),\n      spin: rand(-11, 11),\n      flip: rand(0, Math.PI * 2),\n      flipSpeed: rand(7, 15),\n      sway: rand(0, Math.PI * 2),\n      w: shape === \"rect\" ? rand(7, 10) : shape === \"strip\" ? rand(3, 4) : rand(4.5, 6),\n      h: shape === \"rect\" ? rand(4, 6) : shape === \"strip\" ? rand(10, 14) : 0,\n      shape,\n      color: colors[Math.floor(Math.random() * colors.length)],\n      age: 0,\n      life: rand(1.6, 2.4),\n    };\n  });\n}\n\nexport interface UseConfettiOptions {\n  palette?: ConfettiPalette;\n  count?: number;\n}\n\nexport function useConfetti({ palette = \"party\", count = 90 }: UseConfettiOptions = {}) {\n  const reduce = useReducedMotion();\n  const canvas = useRef<HTMLCanvasElement>(null);\n  const swatches = useRef<HTMLSpanElement>(null);\n  const bits = useRef<Bit[]>([]);\n  const raf = useRef(0);\n  const size = useRef({ w: W, ox: W / 2 });\n  const [pops, setPops] = useState(0);\n  const classes = typeof palette === \"string\" ? confettiPalettes[palette] : palette;\n\n  useEffect(() => () => cancelAnimationFrame(raf.current), []);\n\n  useEffect(() => {\n    if (!pops) return;\n    const t = setTimeout(() => setPops(0), 900);\n    return () => clearTimeout(t);\n  }, [pops]);\n\n  const fire = useCallback(() => {\n    if (reduce) {\n      setPops((n) => n + 1);\n      return;\n    }\n    const el = canvas.current;\n    const ctx = el?.getContext(\"2d\");\n    if (!el || !ctx) return;\n    const colors = [...(swatches.current?.children ?? [])].map((s) => getComputedStyle(s).backgroundColor);\n    const anchor = el.parentElement?.getBoundingClientRect();\n    if (!anchor) return;\n    const cx = anchor.left + anchor.width / 2;\n    const left = Math.max(cx - W / 2, 0);\n    const right = Math.min(cx + W / 2, document.documentElement.clientWidth);\n    const cw = Math.max(right - left, 0);\n    const ox = cx - left;\n    const dpr = window.devicePixelRatio || 1;\n    if (!raf.current) {\n      el.style.width = `${cw}px`;\n      el.style.transform = `translate(${-ox}px, ${-OY}px)`;\n      el.style.display = \"block\";\n      el.width = cw * dpr;\n      el.height = H * dpr;\n      size.current = { w: cw, ox };\n    }\n    bits.current.push(...makeBits(count, colors.length ? colors : [\"currentColor\"], size.current.ox));\n    if (raf.current) return;\n    let last = performance.now();\n    const tick = (now: number) => {\n      const dt = Math.min((now - last) / 1000, 1 / 30);\n      last = now;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, size.current.w, H);\n      bits.current = bits.current.filter((b) => b.age < b.life);\n      for (const b of bits.current) {\n        b.age += dt;\n        const drag = Math.exp(-2.3 * dt);\n        b.vx *= drag;\n        b.vy = b.vy * drag + 1100 * dt;\n        b.sway += dt * 5;\n        b.x += (b.vx + Math.sin(b.sway) * 26) * dt;\n        b.y += b.vy * dt;\n        b.rot += b.spin * dt;\n        b.flip += b.flipSpeed * dt;\n        const fade = Math.min(1, (b.life - b.age) / (b.life * 0.3));\n        const edge = Math.min(b.x, size.current.w - b.x, b.y, H - b.y) / EDGE;\n        ctx.globalAlpha = Math.max(Math.min(fade, edge, 1), 0);\n        ctx.fillStyle = b.color;\n        ctx.save();\n        ctx.translate(b.x, b.y);\n        ctx.rotate(b.rot);\n        ctx.scale(1, Math.cos(b.flip));\n        if (b.shape === \"dot\") {\n          ctx.beginPath();\n          ctx.arc(0, 0, b.w / 2, 0, Math.PI * 2);\n          ctx.fill();\n        } else {\n          ctx.fillRect(-b.w / 2, -b.h / 2, b.w, b.h);\n        }\n        ctx.restore();\n      }\n      if (bits.current.length) raf.current = requestAnimationFrame(tick);\n      else {\n        raf.current = 0;\n        el.style.display = \"none\";\n      }\n    };\n    raf.current = requestAnimationFrame(tick);\n  }, [count, reduce]);\n\n  const layer: ReactNode = (\n    <>\n      <span aria-hidden className=\"hidden\" ref={swatches}>\n        {classes.map((c) => (\n          <span className={c} key={c} />\n        ))}\n      </span>\n      <canvas\n        aria-hidden\n        className=\"pointer-events-none absolute top-1/2 left-1/2 z-10 hidden\"\n        ref={canvas}\n        style={{ height: H }}\n      />\n      <AnimatePresence>\n        {pops > 0 && (\n          <motion.span\n            animate={{ opacity: 1, scale: 1 }}\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute top-0 left-1/2 z-10 -mt-3 -ml-3 grid size-6 place-items-center rounded-full bg-success text-white shadow-xs/5\",\n            )}\n            exit={{ opacity: 0, transition: { duration: 0.2 } }}\n            initial={{ opacity: 0, scale: 0.85 }}\n            key={pops}\n            transition={{ duration: 0.18, ease: [0.23, 1, 0.32, 1] }}\n          >\n            <CheckIcon className=\"size-3.5\" strokeWidth={3} />\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </>\n  );\n\n  return { fire, layer };\n}\n\nexport interface ConfettiBurstProps extends UseConfettiOptions {\n  children: ReactNode;\n  className?: string;\n}\n\nexport function ConfettiBurst({ children, className, ...options }: ConfettiBurstProps) {\n  const { fire, layer } = useConfetti(options);\n  return (\n    <span className={cn(\"relative inline-flex\", className)} onClickCapture={fire}>\n      {children}\n      {layer}\n    </span>\n  );\n}\n"
    }
  ]
}