{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confetti",
  "title": "Confetti",
  "description": "High-performance canvas confetti with burst, cannons, fireworks, rain and stream presets. Includes confetti(), useConfetti, ConfettiButton and Confetti.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/confetti/confetti.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useEffectEvent, useRef, type ReactNode, type RefObject } from \"react\"\nimport { motion, useAnimate, type HTMLMotionProps } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\n// ---------------------------------------------------------------- types\n\nexport type ConfettiShape = \"square\" | \"circle\" | \"strip\" | \"star\"\nexport type ConfettiPreset = \"burst\" | \"cannons\" | \"fireworks\" | \"rain\" | \"pride\" | \"stream\"\n\nexport interface ConfettiOptions {\n  /** Effect preset. Default: \"burst\" */\n  preset?: ConfettiPreset\n  /** Particles per shot (per cannon, per firework). For rain and streams this is the total over `duration`. Default: preset-specific (burst 90) */\n  particleCount?: number\n  /** Cone width in degrees. Default: preset-specific (burst 70) */\n  spread?: number\n  /** Launch direction in degrees, 90 is straight up. Default: 90 */\n  angle?: number\n  /** Initial speed in px per frame. Default: preset-specific (burst 45) */\n  startVelocity?: number\n  /** Gravity multiplier. Default: 1 */\n  gravity?: number\n  /** Constant sideways drift, negative drifts left. Default: 0 */\n  drift?: number\n  /** Velocity kept each frame, 0 to 1. Lower stops particles faster. Default: 0.9 */\n  decay?: number\n  /** Particle size multiplier. Default: 1 */\n  scalar?: number\n  /** Particle lifetime in frames (60 per second). Default: 200 */\n  ticks?: number\n  /** Launch point as a fraction of the viewport or container, 0 to 1. Default: { x: 0.5, y: 0.6 } */\n  origin?: { x?: number; y?: number }\n  /** Particle colors. Default: [\"#ff4d12\", \"#ff8a4c\", \"#ffd2b8\", \"#ededed\", \"#8a8a8a\", \"#0a0a0a\"] */\n  colors?: string[]\n  /** Particle shapes, picked at random. Default: [\"square\", \"circle\", \"strip\"] */\n  shapes?: ConfettiShape[]\n  /** Render these emoji as particles instead of shapes. */\n  emoji?: string[]\n  /** Length of timed presets (fireworks, rain, pride, stream) in seconds. Default: preset-specific (2 to 3) */\n  duration?: number\n  /** z-index of the full-viewport canvas. Default: 100 */\n  zIndex?: number\n  /** Do nothing when the user prefers reduced motion. Default: true */\n  disableForReducedMotion?: boolean\n}\n\nexport const CONFETTI_COLORS = [\"#ff4d12\", \"#ff8a4c\", \"#ffd2b8\", \"#ededed\", \"#8a8a8a\", \"#0a0a0a\"]\nconst PRIDE_COLORS = [\"#e40303\", \"#ff8c00\", \"#ffed00\", \"#008026\", \"#004dff\", \"#750787\"]\n\ninterface Particle {\n  x: number\n  y: number\n  vx: number\n  vy: number\n  gravity: number\n  drift: number\n  decay: number\n  size: number\n  color: string\n  shape: ConfettiShape | \"emoji\" | \"rocket\"\n  emoji: string\n  rot: number\n  rotSpeed: number\n  flip: number\n  flipSpeed: number\n  wobble: number\n  wobbleSpeed: number\n  tick: number\n  ticks: number\n  /** Rockets explode into this shot at their apex. */\n  payload: Shot | null\n}\n\ninterface Shot {\n  particleCount: number\n  spread: number\n  angle: number\n  startVelocity: number\n  gravity: number\n  drift: number\n  decay: number\n  scalar: number\n  ticks: number\n  colors: string[]\n  shapes: ConfettiShape[]\n  emoji: string[]\n}\n\n/** Returns false once finished. Runs inside the shared animation frame. */\ntype Emitter = (dt: number) => boolean\n\n// ---------------------------------------------------------------- engine\n\nconst STAR = (() => {\n  if (typeof Path2D === \"undefined\") return null\n  const p = new Path2D()\n  for (let i = 0; i < 10; i++) {\n    const r = i % 2 === 0 ? 1 : 0.45\n    const a = (Math.PI / 5) * i - Math.PI / 2\n    if (i === 0) p.moveTo(Math.cos(a) * r, Math.sin(a) * r)\n    else p.lineTo(Math.cos(a) * r, Math.sin(a) * r)\n  }\n  p.closePath()\n  return p\n})()\n\nconst emojiCache = new Map<string, HTMLCanvasElement>()\nfunction emojiBitmap(char: string) {\n  let c = emojiCache.get(char)\n  if (!c) {\n    c = document.createElement(\"canvas\")\n    c.width = c.height = 64\n    const ctx = c.getContext(\"2d\")\n    if (ctx) {\n      ctx.font = \"52px system-ui, 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif\"\n      ctx.textAlign = \"center\"\n      ctx.textBaseline = \"middle\"\n      ctx.fillText(char, 32, 36)\n    }\n    emojiCache.set(char, c)\n  }\n  return c\n}\n\nconst rand = (min: number, max: number) => min + Math.random() * (max - min)\nconst pick = <T,>(list: T[]) => list[Math.floor(Math.random() * list.length)]\n\nclass Engine {\n  private canvas: HTMLCanvasElement\n  private ctx: CanvasRenderingContext2D | null\n  private particles: Particle[] = []\n  private pool: Particle[] = []\n  private emitters: Emitter[] = []\n  private raf = 0\n  private last = 0\n  private dpr = 1\n  private resizeObserver: ResizeObserver | null = null\n  private idleWaiters: (() => void)[] = []\n  width = 0\n  height = 0\n\n  constructor(\n    private container: HTMLElement | null,\n    zIndex: number,\n    private onIdle: () => void\n  ) {\n    const canvas = document.createElement(\"canvas\")\n    canvas.setAttribute(\"aria-hidden\", \"true\")\n    Object.assign(canvas.style, {\n      position: container ? \"absolute\" : \"fixed\",\n      inset: \"0\",\n      width: \"100%\",\n      height: \"100%\",\n      pointerEvents: \"none\",\n      zIndex: String(zIndex),\n    })\n    if (container && getComputedStyle(container).position === \"static\") container.style.position = \"relative\"\n    ;(container ?? document.body).appendChild(canvas)\n    this.canvas = canvas\n    this.ctx = canvas.getContext(\"2d\")\n    this.resize()\n    if (container && typeof ResizeObserver !== \"undefined\") {\n      this.resizeObserver = new ResizeObserver(this.resize)\n      this.resizeObserver.observe(container)\n    } else {\n      window.addEventListener(\"resize\", this.resize)\n    }\n  }\n\n  private resize = () => {\n    const rect = this.container?.getBoundingClientRect()\n    this.width = rect ? rect.width : window.innerWidth\n    this.height = rect ? rect.height : window.innerHeight\n    this.dpr = Math.min(window.devicePixelRatio || 1, 2)\n    this.canvas.width = Math.max(1, Math.round(this.width * this.dpr))\n    this.canvas.height = Math.max(1, Math.round(this.height * this.dpr))\n  }\n\n  setZIndex(z: number) {\n    this.canvas.style.zIndex = String(z)\n  }\n\n  whenIdle() {\n    return new Promise<void>((resolve) => this.idleWaiters.push(resolve))\n  }\n\n  addEmitter(emitter: Emitter) {\n    this.emitters.push(emitter)\n    this.start()\n  }\n\n  /** Spawn a shot at a pixel position. */\n  shoot(x: number, y: number, s: Shot) {\n    const useEmoji = s.emoji.length > 0\n    for (let i = 0; i < s.particleCount; i++) {\n      const p = this.pool.pop() ?? ({} as Particle)\n      const angle = ((s.angle + rand(-s.spread / 2, s.spread / 2)) * Math.PI) / 180\n      const speed = s.startVelocity * rand(0.5, 1.5)\n      p.x = x\n      p.y = y\n      p.vx = Math.cos(angle) * speed\n      p.vy = -Math.sin(angle) * speed\n      p.gravity = s.gravity\n      p.drift = s.drift\n      p.decay = s.decay\n      p.size = 9 * s.scalar * rand(0.75, 1.25)\n      p.color = pick(s.colors) ?? CONFETTI_COLORS[0]\n      p.shape = useEmoji ? \"emoji\" : (pick(s.shapes) ?? \"square\")\n      p.emoji = useEmoji ? (pick(s.emoji) ?? \"\") : \"\"\n      p.rot = rand(0, Math.PI * 2)\n      p.rotSpeed = rand(-0.12, 0.12)\n      p.flip = rand(0, Math.PI * 2)\n      p.flipSpeed = rand(0.08, 0.2)\n      p.wobble = rand(0, Math.PI * 2)\n      p.wobbleSpeed = rand(0.04, 0.1)\n      p.tick = 0\n      p.ticks = s.ticks * rand(0.85, 1.15)\n      p.payload = null\n      this.particles.push(p)\n    }\n    this.start()\n  }\n\n  /** Launch a rocket from (x, y) that explodes with `payload` near `targetY`. */\n  rocket(x: number, y: number, targetY: number, payload: Shot) {\n    const p = this.pool.pop() ?? ({} as Particle)\n    const g = 0.28\n    Object.assign(p, {\n      x,\n      y,\n      vx: rand(-0.8, 0.8),\n      vy: -Math.sqrt(2 * g * Math.max(40, y - targetY)),\n      gravity: g,\n      drift: 0,\n      decay: 1,\n      size: 3,\n      color: pick(payload.colors) ?? CONFETTI_COLORS[0],\n      shape: \"rocket\",\n      emoji: \"\",\n      rot: 0,\n      rotSpeed: 0,\n      flip: 0,\n      flipSpeed: 0,\n      wobble: 0,\n      wobbleSpeed: 0,\n      tick: 0,\n      ticks: 600,\n      payload,\n    } satisfies Particle)\n    this.particles.push(p)\n    this.start()\n  }\n\n  private start() {\n    if (this.raf) return\n    this.last = performance.now()\n    this.raf = requestAnimationFrame(this.frame)\n  }\n\n  private frame = (now: number) => {\n    const dt = Math.min((now - this.last) / (1000 / 60), 3)\n    this.last = now\n    const ctx = this.ctx\n\n    for (let i = this.emitters.length - 1; i >= 0; i--) {\n      if (!this.emitters[i](dt)) this.emitters.splice(i, 1)\n    }\n\n    if (ctx) {\n      ctx.setTransform(1, 0, 0, 1, 0, 0)\n      ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)\n    }\n\n    const list = this.particles\n    const dpr = this.dpr\n    for (let i = list.length - 1; i >= 0; i--) {\n      const p = list[i]\n      p.tick += dt\n\n      if (p.shape === \"rocket\") {\n        p.vy += p.gravity * dt\n        p.x += p.vx * dt\n        p.y += p.vy * dt\n        if (p.vy >= -0.5 && p.payload) {\n          this.shoot(p.x, p.y, p.payload)\n          this.release(i)\n          continue\n        }\n        if (ctx) {\n          ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n          ctx.globalAlpha = 1\n          ctx.strokeStyle = p.color\n          ctx.lineWidth = 2\n          ctx.lineCap = \"round\"\n          ctx.beginPath()\n          ctx.moveTo(p.x, p.y)\n          ctx.lineTo(p.x - p.vx * 4, p.y - p.vy * 4)\n          ctx.stroke()\n          ctx.fillStyle = p.color\n          ctx.beginPath()\n          ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)\n          ctx.fill()\n        }\n        continue\n      }\n\n      const drag = Math.pow(p.decay, dt)\n      p.vx *= drag\n      p.vy = p.vy * drag + p.gravity * 0.35 * dt\n      p.x += (p.vx + p.drift) * dt\n      p.y += p.vy * dt\n      p.rot += p.rotSpeed * dt\n      p.flip += p.flipSpeed * dt\n      p.wobble += p.wobbleSpeed * dt\n\n      const life = p.tick / p.ticks\n      if (life >= 1 || p.y > this.height + 40 || p.x < -80 || p.x > this.width + 80) {\n        this.release(i)\n        continue\n      }\n      if (!ctx) continue\n\n      ctx.globalAlpha = life > 0.7 ? 1 - (life - 0.7) / 0.3 : 1\n      const x = p.x + Math.cos(p.wobble) * p.size * 0.6\n      const cos = Math.cos(p.rot)\n      const sin = Math.sin(p.rot)\n      const fy = p.shape === \"emoji\" ? 1 : Math.cos(p.flip)\n      ctx.setTransform(dpr * cos, dpr * sin, -dpr * sin * fy, dpr * cos * fy, dpr * x, dpr * p.y)\n\n      const s = p.size\n      if (p.shape === \"emoji\") {\n        ctx.drawImage(emojiBitmap(p.emoji), -s * 1.2, -s * 1.2, s * 2.4, s * 2.4)\n        continue\n      }\n      ctx.fillStyle = p.color\n      if (p.shape === \"circle\") {\n        ctx.beginPath()\n        ctx.arc(0, 0, s * 0.5, 0, Math.PI * 2)\n        ctx.fill()\n      } else if (p.shape === \"strip\") {\n        ctx.fillRect(-s * 0.18, -s * 0.8, s * 0.36, s * 1.6)\n      } else if (p.shape === \"star\" && STAR) {\n        ctx.scale(s * 0.7, s * 0.7)\n        ctx.fill(STAR)\n      } else {\n        ctx.fillRect(-s * 0.5, -s * 0.5, s, s)\n      }\n    }\n\n    if (list.length === 0 && this.emitters.length === 0) {\n      this.raf = 0\n      this.destroy()\n      return\n    }\n    this.raf = requestAnimationFrame(this.frame)\n  }\n\n  private release(i: number) {\n    const list = this.particles\n    const p = list[i]\n    list[i] = list[list.length - 1]\n    list.pop()\n    p.payload = null\n    if (this.pool.length < 1500) this.pool.push(p)\n  }\n\n  destroy() {\n    if (this.raf) cancelAnimationFrame(this.raf)\n    this.raf = 0\n    this.particles.length = 0\n    this.emitters.length = 0\n    this.resizeObserver?.disconnect()\n    window.removeEventListener(\"resize\", this.resize)\n    this.canvas.remove()\n    this.onIdle()\n    const waiters = this.idleWaiters\n    this.idleWaiters = []\n    waiters.forEach((resolve) => resolve())\n  }\n}\n\nlet viewportEngine: Engine | null = null\nconst containerEngines = new WeakMap<HTMLElement, Engine>()\n\nfunction getEngine(container: HTMLElement | null, zIndex: number) {\n  if (!container) {\n    if (!viewportEngine) viewportEngine = new Engine(null, zIndex, () => (viewportEngine = null))\n    else viewportEngine.setZIndex(zIndex)\n    return viewportEngine\n  }\n  let engine = containerEngines.get(container)\n  if (!engine) {\n    engine = new Engine(container, zIndex, () => containerEngines.delete(container))\n    containerEngines.set(container, engine)\n  }\n  return engine\n}\n\n// ---------------------------------------------------------------- presets\n\nconst PRESET_DEFAULTS: Record<ConfettiPreset, { particleCount: number; spread: number; startVelocity: number; duration: number }> = {\n  burst: { particleCount: 90, spread: 70, startVelocity: 45, duration: 0 },\n  cannons: { particleCount: 70, spread: 55, startVelocity: 55, duration: 0 },\n  fireworks: { particleCount: 70, spread: 360, startVelocity: 26, duration: 2.2 },\n  rain: { particleCount: 220, spread: 20, startVelocity: 2, duration: 3 },\n  pride: { particleCount: 240, spread: 55, startVelocity: 50, duration: 2.5 },\n  stream: { particleCount: 200, spread: 55, startVelocity: 50, duration: 2.5 },\n}\n\nfunction prefersReducedMotion() {\n  return typeof window !== \"undefined\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\n\nfunction run(options: ConfettiOptions, container: HTMLElement | null): Promise<void> {\n  if (typeof window === \"undefined\") return Promise.resolve()\n  const { disableForReducedMotion = true, preset = \"burst\", zIndex = 100 } = options\n  if (disableForReducedMotion && prefersReducedMotion()) return Promise.resolve()\n\n  const d = PRESET_DEFAULTS[preset] ?? PRESET_DEFAULTS.burst\n  const shot: Shot = {\n    particleCount: Math.max(0, Math.round(options.particleCount ?? d.particleCount)),\n    spread: options.spread ?? d.spread,\n    angle: options.angle ?? 90,\n    startVelocity: options.startVelocity ?? d.startVelocity,\n    gravity: options.gravity ?? 1,\n    drift: options.drift ?? 0,\n    decay: Math.min(Math.max(options.decay ?? 0.9, 0), 1),\n    scalar: options.scalar ?? 1,\n    ticks: options.ticks ?? 200,\n    colors: options.colors?.length ? options.colors : preset === \"pride\" ? PRIDE_COLORS : CONFETTI_COLORS,\n    shapes: options.shapes?.length ? options.shapes : [\"square\", \"circle\", \"strip\"],\n    emoji: options.emoji ?? [],\n  }\n  const duration = (options.duration ?? d.duration) * 60 // frames\n  const engine = getEngine(container, zIndex)\n  const W = engine.width\n  const H = engine.height\n  const ox = (options.origin?.x ?? 0.5) * W\n  const oy = (options.origin?.y ?? 0.6) * H\n\n  /** Emits `total` particles evenly over `duration` frames via `spawn(count)`. */\n  const timed = (total: number, spawn: (count: number) => void): Emitter => {\n    let elapsed = 0\n    let emitted = 0\n    return (dt) => {\n      elapsed += dt\n      const due = Math.min(total, Math.round((elapsed / Math.max(duration, 1)) * total))\n      if (due > emitted) spawn(due - emitted)\n      emitted = due\n      return elapsed < duration && emitted < total\n    }\n  }\n\n  switch (preset) {\n    case \"cannons\": {\n      // particleCount is per cannon\n      engine.shoot(0, H, { ...shot, angle: options.angle ?? 60 })\n      engine.shoot(W, H, { ...shot, angle: 180 - (options.angle ?? 60) })\n      break\n    }\n    case \"fireworks\": {\n      const rockets = Math.max(3, Math.round(duration / 22))\n      const schedule = Array.from({ length: rockets }, (_, i) => (i / rockets) * duration + rand(0, 10))\n      let elapsed = 0\n      let next = 0\n      engine.addEmitter((dt) => {\n        elapsed += dt\n        while (next < schedule.length && elapsed >= schedule[next]) {\n          const x = W * rand(0.2, 0.8)\n          engine.rocket(x, H + 4, H * rand(0.18, 0.42), {\n            ...shot,\n            ticks: Math.min(shot.ticks, 150),\n            gravity: shot.gravity * 0.8,\n            decay: Math.min(shot.decay, 0.92),\n            colors: [pick(shot.colors) ?? CONFETTI_COLORS[0], pick(shot.colors) ?? CONFETTI_COLORS[1], \"#ffffff\"],\n          })\n          next++\n        }\n        return next < schedule.length\n      })\n      break\n    }\n    case \"rain\": {\n      const drop: Shot = { ...shot, angle: 270, ticks: Math.max(shot.ticks, 600), particleCount: 1 }\n      engine.addEmitter(\n        timed(shot.particleCount, (count) => {\n          for (let i = 0; i < count; i++) engine.shoot(rand(0, W), -20, { ...drop, drift: shot.drift + rand(-0.4, 0.4) })\n        })\n      )\n      break\n    }\n    case \"pride\":\n    case \"stream\": {\n      const side: Shot = { ...shot, particleCount: 1 }\n      const angle = options.angle ?? 60\n      const y = (options.origin?.y ?? 0.65) * H\n      engine.addEmitter(\n        timed(shot.particleCount, (count) => {\n          for (let i = 0; i < count; i++) {\n            if (i % 2 === 0) engine.shoot(0, y, { ...side, angle })\n            else engine.shoot(W, y, { ...side, angle: 180 - angle })\n          }\n        })\n      )\n      break\n    }\n    default:\n      engine.shoot(ox, oy, shot)\n  }\n\n  return engine.whenIdle()\n}\n\n/**\n * Fire confetti on a shared full-viewport canvas. The canvas is created lazily and\n * removed as soon as the last particle is gone. Resolves when the effect finishes.\n */\nexport function confetti(options: ConfettiOptions = {}): Promise<void> {\n  return run(options, null)\n}\n\n/** Stop every viewport effect immediately. */\nconfetti.reset = () => {\n  viewportEngine?.destroy()\n}\n\n// ---------------------------------------------------------------- hook\n\n/**\n * Returns a stable `fire(options)`. Pass a ref to confine confetti to that element\n * (origin becomes relative to it); omit it for the full viewport.\n */\nexport function useConfetti(container?: RefObject<HTMLElement | null>) {\n  useEffect(() => {\n    const el = container?.current\n    return () => {\n      if (el) containerEngines.get(el)?.destroy()\n    }\n  }, [container])\n\n  // Reads the ref at call time, never during render\n  return function fire(options: ConfettiOptions = {}) {\n    return run(options, container?.current ?? null)\n  }\n}\n\n// ---------------------------------------------------------------- button\n\nexport type ConfettiButtonProps = ConfettiOptions &\n  Omit<HTMLMotionProps<\"button\">, \"children\" | \"color\"> & {\n    /** Button label. Default: \"Celebrate\" */\n    children?: ReactNode\n    /** Button background (any CSS color). Default: \"#ff4d12\" */\n    color?: string\n  }\n\nconst OPTION_KEYS = [\n  \"preset\",\n  \"particleCount\",\n  \"spread\",\n  \"angle\",\n  \"startVelocity\",\n  \"gravity\",\n  \"drift\",\n  \"decay\",\n  \"scalar\",\n  \"ticks\",\n  \"origin\",\n  \"colors\",\n  \"shapes\",\n  \"emoji\",\n  \"duration\",\n  \"zIndex\",\n  \"disableForReducedMotion\",\n] as const satisfies readonly (keyof ConfettiOptions)[]\n\nfunction splitOptions<T extends ConfettiOptions>(props: T) {\n  const options: ConfettiOptions = {}\n  const rest: Record<string, unknown> = {}\n  for (const [k, v] of Object.entries(props)) {\n    if ((OPTION_KEYS as readonly string[]).includes(k)) (options as Record<string, unknown>)[k] = v\n    else rest[k] = v\n  }\n  return { options, rest: rest as Omit<T, keyof ConfettiOptions> }\n}\n\n/** A button that bursts confetti from its own position with a springy press. */\nexport function ConfettiButton(props: ConfettiButtonProps) {\n  const { options, rest } = splitOptions(props)\n  const { children = \"Celebrate\", color = \"#ff4d12\", className, style, onClick, disabled, ...buttonProps } = rest\n  const [scope, animate] = useAnimate<HTMLButtonElement>()\n\n  return (\n    <motion.button\n      ref={scope}\n      type=\"button\"\n      disabled={disabled}\n      {...buttonProps}\n      className={cn(\n        \"relative inline-flex h-11 items-center justify-center gap-2 rounded-full px-6 text-sm font-medium text-white outline-none select-none\",\n        \"shadow-[0_1px_0_0_rgba(255,255,255,0.25)_inset,0_10px_24px_-10px_var(--cb-color)] focus-visible:ring-2 focus-visible:ring-[color:var(--cb-color)] focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n      style={{ backgroundColor: color, [\"--cb-color\" as string]: color, ...style }}\n      whileHover={{ y: -1 }}\n      whileTap={{ scale: 0.92, y: 1 }}\n      transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n      onClick={(e) => {\n        onClick?.(e)\n        if (e.defaultPrevented) return\n        const el = e.currentTarget\n        const rect = el.getBoundingClientRect()\n        const origin = options.origin ?? {\n          x: (rect.left + rect.width / 2) / window.innerWidth,\n          y: (rect.top + rect.height / 2) / window.innerHeight,\n        }\n        void confetti({ ...options, origin })\n        if (!prefersReducedMotion()) {\n          animate(el, { scale: [0.9, 1.08, 1] }, { duration: 0.45, ease: [0.22, 1, 0.36, 1] })\n        }\n      }}\n    >\n      {children}\n    </motion.button>\n  )\n}\n\n// ---------------------------------------------------------------- declarative\n\nexport interface ConfettiProps extends ConfettiOptions {\n  /** When to fire: on mount, when scrolled into view, or when `active` becomes true. Default: \"manual\" */\n  trigger?: \"mount\" | \"inView\" | \"manual\"\n  /** Fires every time this flips to true (manual trigger). Default: false */\n  active?: boolean\n  /** Confine the effect to this element instead of the viewport. */\n  container?: RefObject<HTMLElement | null>\n}\n\n/** Declarative confetti. Renders an invisible marker and fires based on `trigger`. */\nexport function Confetti({ trigger = \"manual\", active = false, container, ...options }: ConfettiProps) {\n  const markerRef = useRef<HTMLSpanElement>(null)\n  const fire = useConfetti(container)\n  const shoot = useEffectEvent(() => {\n    void fire(options)\n  })\n\n  useEffect(() => {\n    if (trigger === \"mount\") shoot()\n  }, [trigger])\n\n  useEffect(() => {\n    if (trigger === \"manual\" && active) shoot()\n  }, [trigger, active])\n\n  useEffect(() => {\n    const el = markerRef.current\n    if (trigger !== \"inView\" || !el) return\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        if (entry?.isIntersecting) {\n          shoot()\n          io.disconnect()\n        }\n      },\n      { threshold: 0.5 }\n    )\n    io.observe(el)\n    return () => io.disconnect()\n  }, [trigger])\n\n  return <span ref={markerRef} aria-hidden=\"true\" className=\"pointer-events-none block h-px w-px\" />\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/confetti",
  "type": "registry:component"
}