{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confetti",
  "title": "Confetti",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Confetti in your theme colors: fireConfetti(), a useConfetti() hook that fires from an element, and a ConfettiButton. Presets for burst, side cannons, stars and fireworks; nothing fires under reduced motion.",
  "dependencies": [
    "canvas-confetti@^1.9.3",
    "class-variance-authority"
  ],
  "devDependencies": [
    "@types/canvas-confetti"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/color.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/confetti.tsx",
      "content": "// Ballmac UI: Confetti. https://ui.ballmac.com/components/confetti\n\"use client\"\n\nimport * as React from \"react\"\nimport confetti from \"canvas-confetti\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { buttonVariants } from \"@/components/ballmac/button\"\nimport { cssColorToRgba } from \"@/lib/ballmac/color\"\n\ntype ConfettiPreset = \"burst\" | \"sides\" | \"stars\" | \"fireworks\"\n\ntype FireConfettiOptions = Omit<confetti.Options, \"colors\" | \"origin\"> & {\n  /** Shape of the effect. */\n  preset?: ConfettiPreset\n  /** Theme variables (\"--chart-1\") or CSS colors. Defaults to the five chart tokens. */\n  colors?: string[]\n  /** Where the burst starts, 0–1 of the viewport. Ignored when `element` is set. */\n  origin?: { x?: number; y?: number }\n  /** Start the burst from the center of this element. */\n  element?: Element | null\n}\n\nconst DEFAULT_COLORS = [\"--chart-1\", \"--chart-2\", \"--chart-3\", \"--chart-4\", \"--chart-5\"]\nconst REDUCED_QUERY = \"(prefers-reduced-motion: reduce)\"\n\nfunction prefersReducedMotion() {\n  return typeof window !== \"undefined\" && !!window.matchMedia?.(REDUCED_QUERY).matches\n}\n\n/** canvas-confetti only parses hex strings, so theme colors are resolved and converted first. */\nfunction toHex(color: string) {\n  const [r, g, b] = cssColorToRgba(document.documentElement, color)\n  return \"#\" + [r, g, b].map((v) => Math.round(v * 255).toString(16).padStart(2, \"0\")).join(\"\")\n}\n\nfunction originOf(element: Element) {\n  const rect = element.getBoundingClientRect()\n  return {\n    x: (rect.left + rect.width / 2) / window.innerWidth,\n    y: (rect.top + rect.height / 2) / window.innerHeight,\n  }\n}\n\n/**\n * Fires confetti in theme colors. Resolves when the particles have settled.\n * Does nothing (and resolves at once) on the server or when the user prefers reduced motion.\n */\nfunction fireConfetti({ preset = \"burst\", colors, origin, element, ...options }: FireConfettiOptions = {}): Promise<void> {\n  if (typeof window === \"undefined\" || prefersReducedMotion()) return Promise.resolve()\n  const palette = (colors ?? DEFAULT_COLORS).map(toHex)\n  const from = { x: 0.5, y: 0.6, ...(element ? originOf(element) : origin) }\n  const base: confetti.Options = { colors: palette, disableForReducedMotion: true, zIndex: 100, ...options }\n  const shots: confetti.Options[] = []\n\n  if (preset === \"sides\") {\n    shots.push(\n      { ...base, particleCount: 60, angle: 60, spread: 55, startVelocity: 55, origin: { x: 0, y: 0.75 } },\n      { ...base, particleCount: 60, angle: 120, spread: 55, startVelocity: 55, origin: { x: 1, y: 0.75 } }\n    )\n  } else if (preset === \"stars\") {\n    const star = { ...base, shapes: [\"star\"] as confetti.Shape[], spread: 360, ticks: 70, gravity: 0, decay: 0.94, startVelocity: 22, origin: from }\n    shots.push({ ...star, particleCount: 36, scalar: 1.1 }, { ...star, particleCount: 14, scalar: 0.7, shapes: [\"circle\"] })\n  } else if (preset === \"fireworks\") {\n    for (let i = 0; i < 4; i++) {\n      shots.push({\n        ...base,\n        particleCount: 50,\n        spread: 360,\n        startVelocity: 28,\n        ticks: 70,\n        origin: { x: 0.2 + Math.random() * 0.6, y: 0.2 + Math.random() * 0.3 },\n      })\n    }\n  } else {\n    shots.push(\n      { ...base, particleCount: 70, spread: 70, startVelocity: 38, scalar: 0.9, origin: from },\n      { ...base, particleCount: 30, spread: 110, startVelocity: 24, scalar: 0.7, decay: 0.92, origin: from }\n    )\n  }\n\n  // Fireworks go off one after another; other presets fire together.\n  const stagger = preset === \"fireworks\" ? 260 : 0\n  return Promise.all(\n    shots.map(\n      (shot, i) =>\n        new Promise<void>((resolve) => {\n          const run = () => Promise.resolve(confetti(shot)).then(() => resolve(), () => resolve())\n          if (stagger && i) window.setTimeout(run, i * stagger)\n          else run()\n        })\n    )\n  ).then(() => undefined)\n}\n\n/** Stable helpers for firing confetti from event handlers. */\nfunction useConfetti() {\n  return React.useMemo(\n    () => ({\n      /** Fires confetti; see fireConfetti for options. */\n      fire: fireConfetti,\n      /** Fires confetti from the center of an element, such as the button that was clicked. */\n      fireFrom: (element: Element | null, options?: Omit<FireConfettiOptions, \"element\">) =>\n        fireConfetti({ ...options, element }),\n    }),\n    []\n  )\n}\n\ntype ConfettiButtonProps = React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    /** Confetti options; the burst starts from the button unless `origin` is set. */\n    options?: Omit<FireConfettiOptions, \"element\">\n  }\n\nfunction ConfettiButton({ options, variant, size, shape, className, onClick, type = \"button\", ...props }: ConfettiButtonProps) {\n  return (\n    <button\n      data-slot=\"confetti-button\"\n      type={type}\n      className={cn(buttonVariants({ variant, size, shape }), className)}\n      onClick={(event) => {\n        onClick?.(event)\n        if (!event.defaultPrevented) {\n          void fireConfetti({ ...options, element: options?.origin ? undefined : event.currentTarget })\n        }\n      }}\n      {...props}\n    />\n  )\n}\n\nexport { ConfettiButton, fireConfetti, useConfetti, type ConfettiButtonProps, type ConfettiPreset, type FireConfettiOptions }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/confetti.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "confetti",
      "celebration",
      "success",
      "canvas",
      "button",
      "motion"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "Call fireConfetti({ preset, colors, element }) from an event handler, or useConfetti().fireFrom(buttonRef.current) after an async success. <ConfettiButton options={{ preset: \"stars\" }}> is a Ballmac button that bursts from itself on click. Colors default to --chart-1…5.",
      "whenToUse": [
        "Rewarding a finished milestone: first deploy, upgrade, completed onboarding",
        "Success states after a long async action",
        "Playful marketing buttons (sparingly)"
      ],
      "whenNotToUse": [
        "Routine actions such as saving a form (use a toast or inline check)",
        "As the only success signal; always show a text confirmation too"
      ],
      "composesWith": [
        "button",
        "number-ticker"
      ],
      "a11y": [],
      "customization": [
        "preset: burst | sides | stars | fireworks",
        "colors: theme variables (\"--chart-2\") or CSS colors",
        "Any canvas-confetti option (particleCount, spread, scalar, ticks…) passes through",
        "ConfettiButton takes the button variants (variant, size, shape)",
        "Reduced motion: fireConfetti resolves immediately without drawing"
      ]
    },
    "examples": [
      "confetti-demo",
      "confetti-presets"
    ],
    "url": "https://ui.ballmac.com/components/confetti"
  },
  "categories": [
    "motion"
  ],
  "type": "registry:ui"
}