{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confetti-button",
  "type": "registry:ui",
  "title": "Confetti Button",
  "description": "iOS 26 liquid-glass pill that erupts spring-physics confetti particles on click.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/confetti-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\n\n/* ── sound ── */\nlet _a: AudioContext, _b: AudioBuffer;\nconst tick = () => {\n  if (typeof window === \"undefined\") return;\n  if (!_a) {\n    _a = new AudioContext();\n    _b = _a.createBuffer(1, (_a.sampleRate * 0.003) | 0, _a.sampleRate);\n    const d = _b.getChannelData(0);\n    for (let i = 0; i < d.length; i++)\n      d[i] = (Math.random() * 2 - 1) * (1 - i / d.length) ** 4;\n  }\n  const s = _a.createBufferSource();\n  s.buffer = _b;\n  const g = _a.createGain();\n  g.gain.value = 0.08;\n  s.connect(g).connect(_a.destination);\n  s.start();\n};\n\n/* ── theme ── */\nconst CSS = `\n.cf{\n  --cf-glass:linear-gradient(180deg,rgba(255,255,255,0.78),rgba(255,255,255,0.62));\n  --cf-border:rgba(0,0,0,0.06);\n  --cf-shadow:0 0 1px rgba(0,0,0,0.04),0 2px 8px rgba(0,0,0,0.04),inset 0 1px 0 rgba(255,255,255,0.8);\n  --cf-hi:rgba(0,0,0,0.88)\n}\n.dark .cf,[data-theme=\"dark\"] .cf{\n  --cf-glass:linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02));\n  --cf-border:rgba(255,255,255,0.07);\n  --cf-shadow:0 1px 3px rgba(0,0,0,0.08),inset 0 1px 0 rgba(255,255,255,0.04);\n  --cf-hi:rgba(255,255,255,0.88)\n}`;\n\n/* ── confetti colors ── */\nconst COLORS = [\n  \"#FF6B6B\",\n  \"#4ECDC4\",\n  \"#45B7D1\",\n  \"#96CEB4\",\n  \"#FFEAA7\",\n  \"#DDA0DD\",\n];\n\ninterface Particle {\n  id: number;\n  x: number;\n  y: number;\n  rotate: number;\n  color: string;\n  size: number;\n  shape: \"circle\" | \"square\";\n}\n\n/* ── component ── */\nexport interface ConfettiButtonProps {\n  label?: string;\n  onClick?: () => void;\n  sound?: boolean;\n  style?: React.CSSProperties;\n}\n\nexport default function ConfettiButton({\n  label = \"Celebrate\",\n  onClick,\n  sound = true,\n  style,\n}: ConfettiButtonProps) {\n  const [particles, setParticles] = React.useState<Particle[]>([]);\n\n  const fire = () => {\n    const batch: Particle[] = Array.from({ length: 24 }, (_, i) => ({\n      id: Date.now() + i,\n      x: (Math.random() - 0.5) * 140,\n      y: -(40 + Math.random() * 80),\n      rotate: Math.random() * 540 - 270,\n      color: COLORS[Math.floor(Math.random() * COLORS.length)],\n      size: 4 + Math.random() * 4,\n      shape: Math.random() > 0.5 ? \"circle\" : \"square\",\n    }));\n    setParticles(batch);\n    if (sound) tick();\n    onClick?.();\n    setTimeout(() => setParticles([]), 900);\n  };\n\n  return (\n    <>\n      <style dangerouslySetInnerHTML={{ __html: CSS }} />\n      <div style={{ position: \"relative\", display: \"inline-block\" }}>\n        <motion.button\n          className=\"cf\"\n          onClick={fire}\n          whileHover={{ scale: 1.04 }}\n          whileTap={{ scale: 0.92 }}\n          transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n          style={{\n            display: \"inline-flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            padding: \"8px 18px\",\n            borderRadius: 10,\n            border: \"1px solid var(--cf-border)\",\n            background: \"var(--cf-glass)\",\n            boxShadow: \"var(--cf-shadow)\",\n            backdropFilter: \"blur(24px)\",\n            WebkitBackdropFilter: \"blur(24px)\",\n            color: \"var(--cf-hi)\",\n            fontSize: 13,\n            fontWeight: 500,\n            cursor: \"pointer\",\n            outline: \"none\",\n            userSelect: \"none\",\n            ...style,\n          }}\n        >\n          {label}\n        </motion.button>\n\n        {/* particles */}\n        <AnimatePresence>\n          {particles.map((p) => (\n            <motion.span\n              key={p.id}\n              initial={{\n                x: 0,\n                y: 0,\n                scale: 1,\n                opacity: 1,\n                rotate: 0,\n              }}\n              animate={{\n                x: p.x,\n                y: p.y,\n                scale: 0,\n                opacity: 0,\n                rotate: p.rotate,\n              }}\n              exit={{ opacity: 0 }}\n              transition={{\n                type: \"spring\",\n                stiffness: 120,\n                damping: 14,\n                mass: 0.8,\n              }}\n              style={{\n                position: \"absolute\",\n                left: \"50%\",\n                top: \"50%\",\n                width: p.size,\n                height: p.size,\n                marginLeft: -p.size / 2,\n                marginTop: -p.size / 2,\n                borderRadius: p.shape === \"circle\" ? \"50%\" : 1,\n                background: p.color,\n                pointerEvents: \"none\",\n              }}\n            />\n          ))}\n        </AnimatePresence>\n      </div>\n    </>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ruixen/confetti-button.tsx"
    }
  ]
}