{
  "name": "button-confetti",
  "type": "registry:ui",
  "tier": "free",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.craftui.space/r/craftui-base.json",
    "craftui-base"
  ],
  "files": [
    {
      "path": "components/craftui/ui/button/confetti.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\nexport interface ConfettiButtonProps\n  extends React.ComponentProps<typeof Button> {\n  /** Number of confetti pieces launched per click. */\n  particleCount?: number;\n  /** Base hue (0-360) — the palette spans a fixed spread of hues from here. */\n  baseHue?: number;\n  /** Max travel distance in px before gravity pulls pieces back down. */\n  spread?: number;\n  /** Total burst duration in seconds. */\n  duration?: number;\n}\n\ninterface Particle {\n  id: number;\n  hue: number;\n  shape: \"rect\" | \"circle\";\n  angle: number;\n  distance: number;\n  rotateTarget: number;\n  size: number;\n  delay: number;\n}\n\ninterface Burst {\n  id: number;\n  particles: Particle[];\n}\n\n// Hue offsets applied to baseHue, cycled across particles — gives a\n// multi-color confetti mix instead of monochrome dots.\nconst HUE_OFFSETS = [0, 40, 90, 150, 200, 280];\n\nfunction createBurst(\n  id: number,\n  count: number,\n  baseHue: number,\n  spread: number,\n): Particle[] {\n  return Array.from({ length: count }, (_, i) => {\n    // Degrees, screen coords (0=right, 90=down) — 200..340 fans mostly\n    // upward, matching how confetti pops off a click before falling.\n    const angle = 200 + Math.random() * 140;\n    return {\n      id: id * 1000 + i,\n      hue: (baseHue + HUE_OFFSETS[i % HUE_OFFSETS.length]) % 360,\n      shape: i % 2 === 0 ? \"rect\" : \"circle\",\n      angle,\n      distance: spread * (0.5 + Math.random() * 0.5),\n      rotateTarget:\n        (Math.random() > 0.5 ? 1 : -1) * (180 + Math.random() * 360),\n      size: 5 + Math.random() * 4,\n      delay: Math.random() * 0.05,\n    };\n  });\n}\n\nfunction ConfettiParticle({\n  particle,\n  spread,\n  duration,\n}: {\n  particle: Particle;\n  spread: number;\n  duration: number;\n}) {\n  const rad = (particle.angle * Math.PI) / 180;\n  const targetX = Math.cos(rad) * particle.distance;\n  const peakY = Math.sin(rad) * particle.distance;\n  const fallY = peakY + spread * 0.9; // gravity: keeps falling past the peak\n\n  return (\n    <motion.span\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute left-1/2 top-1/2\"\n      style={{\n        width: particle.size,\n        height:\n          particle.shape === \"rect\" ? particle.size * 1.8 : particle.size,\n        borderRadius: particle.shape === \"circle\" ? \"50%\" : 2,\n        background: `hsl(${particle.hue} 75% 60%)`,\n        translate: \"-50% -50%\",\n      }}\n      initial={{ x: 0, y: 0, opacity: 1, rotate: 0, scale: 1 }}\n      animate={{\n        x: [0, targetX],\n        y: [0, peakY, fallY],\n        rotate: [0, particle.rotateTarget],\n        opacity: [1, 1, 0],\n        scale: [1, 1, 0.6],\n      }}\n      transition={{\n        x: { duration, delay: particle.delay, ease: \"easeOut\" },\n        y: {\n          duration,\n          delay: particle.delay,\n          ease: [\"easeOut\", \"easeIn\"],\n          times: [0, 0.4, 1],\n        },\n        rotate: { duration, delay: particle.delay, ease: \"easeOut\" },\n        opacity: { duration, delay: particle.delay, times: [0, 0.7, 1] },\n        scale: { duration, delay: particle.delay, times: [0, 0.7, 1] },\n      }}\n    />\n  );\n}\n\nexport const ConfettiButton = React.forwardRef<\n  HTMLButtonElement,\n  ConfettiButtonProps\n>(function ConfettiButton(\n  {\n    particleCount = 14,\n    baseHue = 200,\n    spread = 60,\n    duration = 0.8,\n    className,\n    children = \"Confetti\",\n    onClick,\n    ...props\n  },\n  ref,\n) {\n  const reduced = useReducedMotion();\n  const [bursts, setBursts] = React.useState<Burst[]>([]);\n  const nextId = React.useRef(0);\n  const timers = React.useRef<ReturnType<typeof setTimeout>[]>([]);\n\n  React.useEffect(() => {\n    const active = timers.current;\n    return () => active.forEach(clearTimeout);\n  }, []);\n\n  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n    if (!reduced) {\n      const id = nextId.current++;\n      setBursts((b) => [\n        ...b,\n        { id, particles: createBurst(id, particleCount, baseHue, spread) },\n      ]);\n      timers.current.push(\n        setTimeout(() => {\n          setBursts((b) => b.filter((burst) => burst.id !== id));\n        }, (duration + 0.2) * 1000),\n      );\n    }\n    onClick?.(e);\n  };\n\n  return (\n    <Button\n      ref={ref}\n      className={cn(\"relative isolate overflow-visible rounded-[12px]\", className)}\n      onClick={handleClick}\n      {...props}\n    >\n      {children}\n      {bursts.flatMap((burst) =>\n        burst.particles.map((particle) => (\n          <ConfettiParticle\n            key={particle.id}\n            particle={particle}\n            spread={spread}\n            duration={duration}\n          />\n        )),\n      )}\n    </Button>\n  );\n});\n",
      "type": "registry:ui",
      "target": "~/components/craftui/ui/button/confetti.tsx"
    }
  ],
  "meta": {
    "displayName": "Button - Confetti Burst",
    "description": "On click, a burst of color-varied confetti pieces launches from the button center — each with its own angle, distance and rotation — arcing up and falling back down under simulated gravity.",
    "category": "button",
    "tags": [
      "button",
      "cta",
      "hover",
      "click",
      "sparkle",
      "particles",
      "confetti",
      "scramble",
      "offset-path",
      "whimsical"
    ],
    "animationType": "keyframes"
  }
}