{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"burn-button","type":"registry:ui","title":"Burn Button","description":"Hold to light a spark that burns along the border; if it makes it all the way round, it bursts. Works with Space and Enter.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/burn-button.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  useEffect,\n  useRef,\n  useState,\n  type ButtonHTMLAttributes,\n  type CSSProperties,\n  type KeyboardEvent,\n  type PointerEvent,\n} from \"react\";\n\nexport interface BurnButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Ms the button must be held for the fuse to burn all the way round. */\n  duration?: number;\n  /** Spark and burst color. */\n  color?: string;\n  /** Called once the fuse finishes burning. */\n  onComplete?: () => void;\n}\n\nconst SPARKS = 14;\nconst EMBERS = 5;\nconst css = `\n@keyframes fuse-button-spark{from{transform:translate(-50%,-50%) scale(1);opacity:1}to{transform:translate(calc(-50% + var(--dx)),calc(-50% + var(--dy))) scale(.2);opacity:0}}\n@keyframes fuse-button-ring{from{transform:translate(-50%,-50%) scale(.15);opacity:.9}to{transform:translate(-50%,-50%) scale(1);opacity:0}}\n@keyframes fuse-button-flash{from{opacity:.45}to{opacity:0}}\n@keyframes fuse-button-regrow{from{opacity:0}to{opacity:1}}`;\n\n/** Pill outline starting at top center, running clockwise. */\nfunction pill(w: number, h: number, inset = 1) {\n  const r = h / 2 - inset;\n  const l = inset + r;\n  const rt = w - inset - r;\n  const t = inset;\n  const b = h - inset;\n  return `M${w / 2} ${t}H${rt}A${r} ${r} 0 0 1 ${rt} ${b}H${l}A${r} ${r} 0 0 1 ${l} ${t}Z`;\n}\n\nexport function BurnButton({\n  duration = 1600,\n  color = \"#fb923c\",\n  onComplete,\n  className,\n  children,\n  disabled,\n  onPointerDown,\n  onPointerUp,\n  onPointerLeave,\n  onPointerCancel,\n  onKeyDown,\n  onKeyUp,\n  ...props\n}: BurnButtonProps) {\n  const btn = useRef<HTMLButtonElement>(null);\n  const fuse = useRef<SVGPathElement>(null);\n  const spark = useRef<SVGGElement>(null);\n  const loop = useRef({ p: 0, holding: false, raf: 0, last: 0, calm: false });\n  const [size, setSize] = useState({ w: 0, h: 0 });\n  const [bursts, setBursts] = useState(0);\n\n  useEffect(() => {\n    const el = btn.current;\n    if (!el) return;\n    const ro = new ResizeObserver(() => setSize({ w: el.offsetWidth, h: el.offsetHeight }));\n    ro.observe(el);\n    const st = loop.current;\n    return () => {\n      ro.disconnect();\n      cancelAnimationFrame(st.raf);\n    };\n  }, []);\n\n  // Direct DOM writes: burnt part hidden via dash offset, spark parked at the burn point.\n  const draw = (p: number) => {\n    const path = fuse.current;\n    const g = spark.current;\n    if (!path || !g) return;\n    const len = path.getTotalLength();\n    path.style.strokeDasharray = `${len} ${len}`;\n    path.style.strokeDashoffset = `${-p * len}`;\n    const pt = path.getPointAtLength(p * len);\n    g.setAttribute(\"transform\", `translate(${pt.x} ${pt.y})`);\n    g.style.opacity = p > 0 ? \"1\" : \"0\";\n    if (loop.current.calm) return;\n    g.querySelectorAll<SVGCircleElement>(\"[data-ember]\").forEach((c) => {\n      const a = Math.random() * Math.PI * 2;\n      const d = 4 + Math.random() * 10;\n      c.setAttribute(\"cx\", `${Math.cos(a) * d}`);\n      c.setAttribute(\"cy\", `${Math.sin(a) * d}`);\n      c.style.opacity = `${Math.random()}`;\n    });\n  };\n\n  const tick = (now: number) => {\n    const st = loop.current;\n    const dt = Math.min(50, Math.max(0, now - st.last)); // rAF time can precede performance.now()\n    st.last = now;\n    // Burns forward while held, snaps back quickly when released early.\n    st.p += st.holding ? dt / duration : -dt / 280;\n    if (st.p >= 1) {\n      st.p = 0;\n      st.holding = false;\n      st.raf = 0;\n      draw(0);\n      setBursts((b) => b + 1);\n      onComplete?.();\n      return;\n    }\n    if (st.p <= 0 && !st.holding) {\n      st.p = 0;\n      st.raf = 0;\n      draw(0);\n      return;\n    }\n    draw(st.p);\n    st.raf = requestAnimationFrame(tick);\n  };\n\n  const start = () => {\n    const st = loop.current;\n    if (disabled || st.holding) return;\n    st.holding = true;\n    st.calm = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    if (!st.raf) {\n      st.last = performance.now();\n      st.raf = requestAnimationFrame(tick);\n    }\n  };\n  const stop = () => {\n    loop.current.holding = false;\n  };\n\n  const key = (e: KeyboardEvent<HTMLButtonElement>, down: boolean) => {\n    if (e.key !== \" \" && e.key !== \"Enter\") return;\n    e.preventDefault();\n    if (down && !e.repeat) start();\n    if (!down) stop();\n  };\n\n  const { w, h } = size;\n  return (\n    <button\n      ref={btn}\n      type=\"button\"\n      disabled={disabled}\n      {...props}\n      onPointerDown={(e: PointerEvent<HTMLButtonElement>) => {\n        if (e.button === 0) start();\n        onPointerDown?.(e);\n      }}\n      onPointerUp={(e) => {\n        stop();\n        onPointerUp?.(e);\n      }}\n      onPointerLeave={(e) => {\n        stop();\n        onPointerLeave?.(e);\n      }}\n      onPointerCancel={(e) => {\n        stop();\n        onPointerCancel?.(e);\n      }}\n      onKeyDown={(e) => {\n        key(e, true);\n        onKeyDown?.(e);\n      }}\n      onKeyUp={(e) => {\n        key(e, false);\n        onKeyUp?.(e);\n      }}\n      onContextMenu={(e) => e.preventDefault()}\n      className={`group relative inline-flex h-12 touch-none select-none items-center justify-center gap-2 rounded-full bg-card px-8 text-sm font-medium text-foreground shadow-[inset_0_1px_0_rgb(255_255_255/.06),0_10px_30px_-14px_rgb(0_0_0/.6)] transition-[scale] duration-200 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring enabled:active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 ${className ?? \"\"}`}\n    >\n      <style href=\"burn-button\" precedence=\"default\">\n        {css}\n      </style>\n      {w > 0 && (\n        <svg aria-hidden width={w} height={h} className=\"pointer-events-none absolute inset-0 overflow-visible\">\n          {/* Burnt track */}\n          <path d={pill(w, h)} fill=\"none\" strokeWidth={1.5} className=\"stroke-border\" />\n          {/* Unburnt fuse; remounted after each burst so it can fade back in. */}\n          <path\n            key={bursts}\n            ref={fuse}\n            d={pill(w, h)}\n            fill=\"none\"\n            strokeWidth={2}\n            strokeLinecap=\"round\"\n            className={`stroke-foreground/40 ${bursts ? \"[animation:fuse-button-regrow_.6s_.35s_ease-out_both]\" : \"\"}`}\n          />\n          <g ref={spark} style={{ opacity: 0 }}>\n            <circle r={13} fill={color} opacity={0.16} />\n            <circle r={6.5} fill={color} opacity={0.5} />\n            <circle r={3} fill=\"#fff\" />\n            {Array.from({ length: EMBERS }, (_, i) => (\n              <circle key={i} data-ember r={1.5} fill={i % 2 ? color : \"#fde68a\"} />\n            ))}\n          </g>\n        </svg>\n      )}\n      {bursts > 0 && (\n        <span key={bursts} aria-hidden className=\"pointer-events-none absolute inset-0\">\n          <span\n            className=\"absolute inset-0 rounded-full [animation:fuse-button-flash_.7s_ease-out_forwards]\"\n            style={{ background: color }}\n          />\n          <span className=\"absolute left-1/2 top-px motion-reduce:hidden\">\n            <span\n              className=\"absolute left-0 top-0 size-20 rounded-full border-2 [animation:fuse-button-ring_.6s_ease-out_forwards]\"\n              style={{ borderColor: color }}\n            />\n            {Array.from({ length: SPARKS }, (_, i) => {\n              const a = (i / SPARKS) * Math.PI * 2 + 0.2;\n              const d = 26 + (i % 3) * 14;\n              return (\n                <span\n                  key={i}\n                  className=\"absolute left-0 top-0 size-2 rounded-full [animation:fuse-button-spark_.7s_cubic-bezier(.15,.8,.3,1)_forwards]\"\n                  style={\n                    {\n                      background: i % 2 ? color : \"#fef3c7\",\n                      boxShadow: `0 0 6px ${color}`,\n                      \"--dx\": `${Math.cos(a) * d}px`,\n                      \"--dy\": `${Math.sin(a) * d}px`,\n                    } as CSSProperties\n                  }\n                />\n              );\n            })}\n          </span>\n        </span>\n      )}\n      <span className=\"relative\">{children}</span>\n    </button>\n  );\n}\n"}]}