# Plume — Animated Button for React

> A celebration button: click it and a seeded plume of glowing sparks erupts upward, arcs out under gravity, and fades while the pill gives a satisfying squash-and-settle. Optionally latch into a success state with a settling checkmark. Seeded canvas particles torn down the instant they fade; under reduced motion the burst becomes a calm glow (plus the check when latched).

- Page: https://crucibleui.com/components/buttons/plume
- Category: [buttons](https://crucibleui.com/components/buttons)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: canvas

## Install

```bash
npx shadcn@latest add @crucible/plume
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/plume`.

## Props (`PlumeProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `colors?` | `string[]` | `["#fff7ed", "#fde68a", "#fbbf24", "#f59e0b"]` | Palette the burst samples from, one color picked per spark. Any CSS hex colors. Defaults to a warm champagne → gold ramp — celebration reads warm. Pass multiple bright hues for confetti. |
| `intensity?` | `number` | `1` | Master energy 0–2: scales spark count, size, and glow. 1 is a generous pop; push to 1.6+ for a firework. |
| `speed?` | `number` | `1` | Multiplies launch velocity — how far the plume throws before gravity reels it back. |
| `particleCount?` | `number` | `34` | Base number of sparks at intensity 1 (the real count scales with `intensity`). |
| `gravity?` | `number` | `1400` | Downward acceleration in px/s² that arcs the sparks back down. |
| `lifetime?` | `number` | `1.1` | Seconds a spark lives before fading out (per-spark jittered). |
| `spread?` | `number` | `150` | Width of the upward launch fan in degrees. 360 fires fully radial. |
| `latch?` | `boolean` | `false` | Latch into a success state after the first celebration: a checkmark settles in and further clicks no longer re-burst (onClick still fires). |
| `paused?` | `boolean` | `false` | Suppress the burst and squash entirely — the button still clicks and latches. |
| `onCelebrate?` | `() => void` | — | Fires each time a celebration triggers (before the burst draws). |
| `maxParticles?` | `number` | `220` | Hard cap on live sparks; oldest are recycled beyond it. |

Also accepts: `React.ComponentPropsWithoutRef<"button">`.

## Usage example

```tsx
import { Plume } from "@/components/crucible/plume";

const LABEL = "text-[10px] font-medium uppercase tracking-[0.2em] text-neutral-600";

export default function PlumeDemo() {
  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.035),transparent_70%)]"
      />
      <div className="relative z-10 flex flex-col items-center gap-10">
        <div className="flex flex-wrap items-end justify-center gap-x-12 gap-y-10">
          <div className="flex flex-col items-center gap-3">
            <Plume>Celebrate</Plume>
            <span className={LABEL}>Default</span>
          </div>
          <div className="flex flex-col items-center gap-3">
            <Plume
              colors={["#a5f3fc", "#c4b5fd", "#f9a8d4", "#fef08a"]}
              intensity={1.5}
              particleCount={44}
            >
              Confetti
            </Plume>
            <span className={LABEL}>Custom colors</span>
          </div>
          <div className="flex flex-col items-center gap-3">
            <Plume latch colors={["#bbf7d0", "#86efac", "#4ade80"]}>
              Complete task
            </Plume>
            <span className={LABEL}>Latching success</span>
          </div>
        </div>
        <p className="text-sm text-neutral-500">Click to celebrate.</p>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through. Pauses offscreen and on hidden tabs, clamps device pixel ratio, and tears down its GL/canvas context on unmount.
