{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confetti",
  "type": "registry:ui",
  "title": "Confetti",
  "description": "Canvas confetti bursts with flipping squares, circles and strips, fired by ref, hook or trigger.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/confetti.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type ConfettiShape = \"mixed\" | \"square\" | \"circle\" | \"strip\";\n\n/** Where a burst starts: an element (its centre) or a point in fractions (0–1) of the canvas. */\nexport type ConfettiOrigin = Element | { x: number; y: number };\n\nexport interface ConfettiOptions {\n  /** Number of pieces in the burst. */\n  count?: number;\n  /** Launch direction in degrees: 90 is straight up, 0 is right, 180 is left. */\n  angle?: number;\n  /** Cone width in degrees around `angle` (360 = all directions). */\n  spread?: number;\n  /** Launch speed in px per frame (at 60fps). */\n  velocity?: number;\n  /** Gravity multiplier. */\n  gravity?: number;\n  /** Air drag: fraction of speed kept per frame (0.8–0.99). */\n  drag?: number;\n  /** Constant sideways push in px per frame²; negative blows left. */\n  wind?: number;\n  /** Piece colours (any CSS colour). */\n  colors?: string[];\n  /** Piece shape. */\n  shape?: ConfettiShape;\n  /** Piece size multiplier. */\n  size?: number;\n  /** Burst origin. */\n  origin?: ConfettiOrigin;\n}\n\nexport interface ConfettiHandle {\n  /** Launches a burst; options override the component's props for this burst only. */\n  fire: (options?: ConfettiOptions) => void;\n  /** Removes every piece immediately. */\n  clear: () => void;\n}\n\nexport interface ConfettiProps\n  extends ConfettiOptions,\n    Omit<React.ComponentPropsWithoutRef<\"canvas\">, \"children\" | \"color\"> {\n  ref?: React.Ref<ConfettiHandle>;\n  /** Controller from `useConfetti()`; an alternative to `ref`. */\n  controller?: ConfettiController;\n  /** Fires a burst whenever this value changes (not on mount). */\n  trigger?: unknown;\n  /** \"fixed\" covers the viewport; \"absolute\" fills the nearest positioned ancestor. */\n  position?: \"fixed\" | \"absolute\";\n  /** Text announced politely to screen readers on every burst. */\n  announce?: string;\n  /** With prefers-reduced-motion: a brief static scatter of pieces, or nothing at all. */\n  reducedMotion?: \"static\" | \"none\";\n}\n\nexport const CONFETTI_COLORS = [\"#FF7448\", \"#B6F3D1\", \"#FFB59B\", \"#5FD3A0\", \"#FFD98A\", \"#F4F1EA\"];\n\nconst DEFAULTS = {\n  count: 120,\n  angle: 90,\n  spread: 70,\n  velocity: 32,\n  gravity: 1,\n  drag: 0.92,\n  wind: 0,\n  shape: \"mixed\" as ConfettiShape,\n  size: 1,\n};\n\nconst MAX_PIECES = 1500;\nconst STATIC_MS = 1600;\n\n/* ---------------------------------- Colour --------------------------------- */\n\nlet probe: CanvasRenderingContext2D | null = null;\n\nfunction resolveColor(input: string): [number, number, number] {\n  probe ??= document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n  if (!probe) return [255, 116, 72];\n  probe.clearRect(0, 0, 1, 1);\n  probe.fillStyle = \"rgba(0, 0, 0, 0)\";\n  probe.fillStyle = input;\n  probe.fillRect(0, 0, 1, 1);\n  const [r, g, b, a] = probe.getImageData(0, 0, 1, 1).data;\n  return a === 0 ? [255, 116, 72] : [r, g, b];\n}\n\n/** Front and back (shaded) fill for each colour, so flipping pieces catch the light. */\nfunction palette(colors: string[]) {\n  return (colors.length ? colors : CONFETTI_COLORS).map((c) => {\n    const [r, g, b] = resolveColor(c);\n    return [`rgb(${r}, ${g}, ${b})`, `rgb(${Math.round(r * 0.68)}, ${Math.round(g * 0.68)}, ${Math.round(b * 0.68)})`] as const;\n  });\n}\n\n/* --------------------------------- Engine ---------------------------------- */\n\ninterface Piece {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  drag: number;\n  rot: number;\n  spin: number;\n  flip: number;\n  flipSpeed: number;\n  wobble: number;\n  wobbleSpeed: number;\n  w: number;\n  h: number;\n  kind: 0 | 1 | 2; // square, circle, strip\n  front: string;\n  back: string;\n  age: number;\n  life: number;\n  gravity: number;\n  wind: number;\n}\n\ntype Resolved = Required<Omit<ConfettiOptions, \"origin\" | \"colors\">> & { colors: string[]; origin: ConfettiOrigin };\n\nfunction createEngine(canvas: HTMLCanvasElement) {\n  const ctx = canvas.getContext(\"2d\");\n  let pieces: Piece[] = [];\n  let raf = 0;\n  let last = 0;\n  let width = 0;\n  let height = 0;\n  let dpr = 1;\n  let staticTimer = 0;\n\n  const resize = () => {\n    const rect = canvas.getBoundingClientRect();\n    width = rect.width;\n    height = rect.height;\n    dpr = Math.min(2, window.devicePixelRatio || 1);\n    const w = Math.max(1, Math.round(width * dpr));\n    const h = Math.max(1, Math.round(height * dpr));\n    if (canvas.width !== w || canvas.height !== h) {\n      canvas.width = w;\n      canvas.height = h;\n    }\n  };\n\n  const originPoint = (origin: ConfettiOrigin) => {\n    if (origin instanceof Element) {\n      const r = origin.getBoundingClientRect();\n      const c = canvas.getBoundingClientRect();\n      return [r.left + r.width / 2 - c.left, r.top + r.height / 2 - c.top] as const;\n    }\n    return [origin.x * width, origin.y * height] as const;\n  };\n\n  const make = (o: Resolved, x: number, y: number, fills: ReturnType<typeof palette>, kindIndex: number): Piece => {\n    const kind: 0 | 1 | 2 =\n      o.shape === \"square\" ? 0 : o.shape === \"circle\" ? 1 : o.shape === \"strip\" ? 2 : ((kindIndex % 3) as 0 | 1 | 2);\n    const s = (6 + Math.random() * 5) * o.size;\n    const dir = ((o.angle + (Math.random() - 0.5) * o.spread) * Math.PI) / 180;\n    const speed = o.velocity * (0.5 + Math.random() * 0.5);\n    const [front, back] = fills[Math.floor(Math.random() * fills.length)];\n    return {\n      x,\n      y,\n      vx: Math.cos(dir) * speed,\n      vy: -Math.sin(dir) * speed,\n      drag: Math.min(0.99, Math.max(0.8, o.drag + (Math.random() - 0.5) * 0.03)),\n      rot: Math.random() * Math.PI * 2,\n      spin: (Math.random() - 0.5) * 0.25,\n      flip: Math.random() * Math.PI * 2,\n      flipSpeed: 0.08 + Math.random() * 0.14,\n      wobble: Math.random() * Math.PI * 2,\n      wobbleSpeed: 0.04 + Math.random() * 0.06,\n      w: kind === 2 ? s * 0.38 : s,\n      h: kind === 2 ? s * 2.1 : kind === 1 ? s : s * 0.72,\n      kind,\n      front,\n      back,\n      age: 0,\n      life: 2600 + Math.random() * 1800,\n      gravity: 0.42 * o.gravity * (0.85 + Math.random() * 0.3),\n      wind: o.wind * 0.06,\n    };\n  };\n\n  const draw = () => {\n    if (!ctx) return;\n    ctx.setTransform(1, 0, 0, 1, 0, 0);\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    for (const p of pieces) {\n      const fade = p.age > p.life * 0.75 ? Math.max(0, 1 - (p.age - p.life * 0.75) / (p.life * 0.25)) : 1;\n      const sy = Math.cos(p.flip); // 3D-ish flip: squash on one axis\n      const cos = Math.cos(p.rot);\n      const sin = Math.sin(p.rot);\n      ctx.setTransform(cos * dpr, sin * dpr, -sin * sy * dpr, cos * sy * dpr, p.x * dpr, p.y * dpr);\n      ctx.globalAlpha = fade;\n      ctx.fillStyle = sy < 0 ? p.back : p.front;\n      if (p.kind === 1) {\n        ctx.beginPath();\n        ctx.arc(0, 0, p.w / 2, 0, Math.PI * 2);\n        ctx.fill();\n      } else {\n        ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);\n      }\n    }\n    ctx.globalAlpha = 1;\n  };\n\n  const frame = (now: number) => {\n    raf = 0;\n    const dt = Math.min(48, last ? now - last : 16.7);\n    last = now;\n    const f = dt / 16.7;\n    const next: Piece[] = [];\n    for (const p of pieces) {\n      const k = Math.pow(p.drag, f);\n      p.vx = p.vx * k + p.wind * f;\n      p.vy = p.vy * k + p.gravity * f;\n      p.wobble += p.wobbleSpeed * f;\n      p.x += (p.vx + Math.sin(p.wobble) * 0.9) * f;\n      p.y += p.vy * f;\n      p.rot += p.spin * f;\n      p.flip += p.flipSpeed * f;\n      p.age += dt;\n      if (p.age < p.life && p.y < height + 40 && p.x > -80 && p.x < width + 80) next.push(p);\n    }\n    pieces = next;\n    draw();\n    if (pieces.length) raf = requestAnimationFrame(frame);\n    else {\n      last = 0;\n      clear(); // settled: release the canvas pixels\n    }\n  };\n\n  const clear = () => {\n    cancelAnimationFrame(raf);\n    raf = 0;\n    last = 0;\n    window.clearTimeout(staticTimer);\n    pieces = [];\n    if (ctx) {\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n    }\n  };\n\n  const fire = (o: Resolved, still: boolean) => {\n    resize();\n    const [x, y] = originPoint(o.origin);\n    const fills = palette(o.colors);\n    if (still) {\n      // Reduced motion: a small static scatter around the origin, cleared after a moment.\n      clear();\n      for (let i = 0; i < 18; i++) {\n        const p = make(o, x, y, fills, i);\n        const a = Math.random() * Math.PI * 2;\n        const d = 24 + Math.random() * 70;\n        p.x += Math.cos(a) * d;\n        p.y += Math.sin(a) * d * 0.7;\n        p.flip = Math.random() * 0.8; // mostly face-on\n        pieces.push(p);\n      }\n      draw();\n      staticTimer = window.setTimeout(clear, STATIC_MS);\n      return;\n    }\n    const n = Math.max(1, Math.min(Math.round(o.count), MAX_PIECES - pieces.length));\n    for (let i = 0; i < n; i++) pieces.push(make(o, x, y, fills, i));\n    if (!raf) raf = requestAnimationFrame(frame);\n  };\n\n  return { fire, clear, resize };\n}\n\n/* ----------------------------- Reduced motion ------------------------------ */\n\nconst reducedMotionQuery = () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n/* -------------------------------- Component -------------------------------- */\n\n/**\n * A canvas confetti layer. Fire bursts imperatively through a ref (or the `useConfetti` hook),\n * or declaratively by changing `trigger`. Pieces are squares, circles or strips with a fake 3D\n * flip, gravity, drag, wobble and wind; the rAF loop only runs while pieces are alive and the\n * canvas is cleared as soon as everything settles.\n */\nexport function Confetti({\n  ref,\n  controller,\n  trigger,\n  position = \"fixed\",\n  announce,\n  reducedMotion = \"static\",\n  count = DEFAULTS.count,\n  angle = DEFAULTS.angle,\n  spread = DEFAULTS.spread,\n  velocity = DEFAULTS.velocity,\n  gravity = DEFAULTS.gravity,\n  drag = DEFAULTS.drag,\n  wind = DEFAULTS.wind,\n  colors = CONFETTI_COLORS,\n  shape = DEFAULTS.shape,\n  size = DEFAULTS.size,\n  origin,\n  className = \"\",\n  style,\n  ...props\n}: ConfettiProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const liveRef = React.useRef<HTMLSpanElement>(null);\n  const engineRef = React.useRef<ReturnType<typeof createEngine> | null>(null);\n  const announceTimer = React.useRef(0);\n\n  // Latest props for imperative calls, kept outside render.\n  const optionsRef = React.useRef<Resolved | null>(null);\n  const settingsRef = React.useRef({ announce, reducedMotion });\n  React.useEffect(() => {\n    optionsRef.current = {\n      count,\n      angle,\n      spread,\n      velocity,\n      gravity,\n      drag,\n      wind,\n      colors,\n      shape,\n      size,\n      origin: origin ?? { x: 0.5, y: 0.6 },\n    };\n    settingsRef.current = { announce, reducedMotion };\n  });\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const engine = createEngine(canvas);\n    engineRef.current = engine;\n    const ro = new ResizeObserver(() => engine.resize());\n    ro.observe(canvas);\n    const timer = announceTimer;\n    return () => {\n      ro.disconnect();\n      engine.clear();\n      window.clearTimeout(timer.current);\n      engineRef.current = null;\n    };\n  }, []);\n\n  const fire = React.useCallback((options?: ConfettiOptions) => {\n    const engine = engineRef.current;\n    const base = optionsRef.current;\n    if (!engine || !base) return;\n    const { announce: text, reducedMotion: rm } = settingsRef.current;\n    const reduce = reducedMotionQuery();\n    if (!reduce || rm === \"static\") {\n      const merged = { ...base, ...Object.fromEntries(Object.entries(options ?? {}).filter(([, v]) => v !== undefined)) } as Resolved;\n      engine.fire(merged, reduce);\n    }\n    const live = liveRef.current;\n    if (text && live) {\n      // Clear first so repeating the same message is announced again.\n      live.textContent = \"\";\n      window.clearTimeout(announceTimer.current);\n      announceTimer.current = window.setTimeout(() => {\n        live.textContent = text;\n      }, 60);\n    }\n  }, []);\n\n  const clear = React.useCallback(() => engineRef.current?.clear(), []);\n\n  React.useImperativeHandle(ref, () => ({ fire, clear }), [fire, clear]);\n\n  React.useEffect(() => {\n    if (!controller) return;\n    controller.attach({ fire, clear });\n    return () => controller.attach(null);\n  }, [controller, fire, clear]);\n\n  // Declarative trigger: fire when the value changes after mount.\n  const lastTrigger = React.useRef(trigger);\n  React.useEffect(() => {\n    if (Object.is(lastTrigger.current, trigger)) return;\n    lastTrigger.current = trigger;\n    fire();\n  }, [trigger, fire]);\n\n  return (\n    <>\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className={`pointer-events-none inset-0 z-50 block h-full w-full ${position === \"fixed\" ? \"fixed\" : \"absolute\"} ${className}`}\n        style={style}\n        {...props}\n      />\n      <span ref={liveRef} role=\"status\" aria-live=\"polite\" className=\"sr-only\" />\n    </>\n  );\n}\n\nexport interface ConfettiController {\n  /** Called by `<Confetti controller={…} />` to connect itself; you rarely need it directly. */\n  attach: (handle: ConfettiHandle | null) => void;\n  fire: (options?: ConfettiOptions) => void;\n  clear: () => void;\n}\n\nfunction createController(): ConfettiController {\n  let handle: ConfettiHandle | null = null;\n  return {\n    attach: (next) => {\n      handle = next;\n    },\n    fire: (options) => handle?.fire(options),\n    clear: () => handle?.clear(),\n  };\n}\n\n/**\n * Convenience hook: `const confetti = useConfetti()`, render `<Confetti controller={confetti} />`,\n * then call `confetti.fire({ origin: event.currentTarget })` from any handler. The returned\n * controller is stable for the component's lifetime.\n */\nexport function useConfetti(): ConfettiController {\n  const [controller] = React.useState(createController);\n  return controller;\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "backgrounds"
  ],
  "docs": "Documentation: https://wancraft.my/components/confetti"
}
