{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "burst",
  "title": "Burst",
  "description": "A short burst of pieces for the moment something finally completes.",
  "registryDependencies": [
    "utils",
    "https://ui.tinkererslabs.com/r/render-surface.json"
  ],
  "files": [
    {
      "path": "registry/default/burst/burst.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  RenderSurface,\n  useThemeColors,\n  type SurfaceColor,\n} from \"@/registry/default/render-surface/render-surface\"\nimport { cn } from \"@/lib/utils\"\n\nexport type BurstOptions = {\n  /** Where the burst starts, in pixels from the top left of the box. */\n  x?: number\n  y?: number\n  count?: number\n}\n\nexport type BurstHandle = {\n  fire: (options?: BurstOptions) => void\n}\n\nexport type BurstProps = React.HTMLAttributes<HTMLDivElement> & {\n  colors?: readonly string[]\n  count?: number\n  /** Pixels per second the pieces leave at. */\n  velocity?: number\n  gravity?: number\n  /** Announced once when a burst is fired. Leave empty for pure decoration. */\n  announce?: string\n}\n\ntype Piece = {\n  x: number\n  y: number\n  vx: number\n  vy: number\n  life: number\n  spin: number\n  angle: number\n  size: number\n  color: SurfaceColor\n}\n\nconst DEFAULT_COLORS = [\"--primary\", \"--foreground\"] as const\n\n/**\n * A short burst of pieces for the moment something completes. It covers its\n * nearest positioned ancestor, draws nothing until fired, and stops its loop\n * once the last piece has fallen out of the box.\n */\nexport const Burst = React.forwardRef<BurstHandle, BurstProps>(function Burst(\n  {\n    colors = DEFAULT_COLORS,\n    count = 60,\n    velocity = 420,\n    gravity = 900,\n    announce,\n    className,\n    ...rootProps\n  },\n  forwardedRef\n) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const pieces = React.useRef<Piece[]>([])\n  const [alive, setAlive] = React.useState(false)\n  const [message, setMessage] = React.useState(\"\")\n\n  const tokens = React.useMemo(\n    () => colors.filter((entry) => entry.startsWith(\"--\")),\n    [colors]\n  )\n\n  const resolved = useThemeColors(rootRef, tokens)\n\n  const palette = React.useMemo(\n    () =>\n      colors\n        .map((entry) => (entry.startsWith(\"--\") ? resolved[entry] : undefined))\n        .filter((entry): entry is SurfaceColor => entry !== undefined),\n    [colors, resolved]\n  )\n\n  const paletteRef = React.useRef(palette)\n  React.useEffect(() => {\n    paletteRef.current = palette\n  })\n\n  React.useImperativeHandle(\n    forwardedRef,\n    () => ({\n      fire: (options) => {\n        const element = rootRef.current\n        if (!element) return\n\n        if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) {\n          if (announce) setMessage(announce)\n          return\n        }\n\n        const rect = element.getBoundingClientRect()\n        const shades = paletteRef.current\n        if (shades.length === 0) return\n\n        const originX = options?.x ?? rect.width / 2\n        const originY = options?.y ?? rect.height / 2\n        const total = options?.count ?? count\n\n        for (let index = 0; index < total; index += 1) {\n          const angle = Math.random() * Math.PI * 2\n          const power = velocity * (0.45 + Math.random() * 0.55)\n\n          pieces.current.push({\n            x: originX,\n            y: originY,\n            vx: Math.cos(angle) * power,\n            vy: Math.sin(angle) * power - velocity * 0.35,\n            life: 0.9 + Math.random() * 0.8,\n            spin: (Math.random() - 0.5) * 14,\n            angle: Math.random() * Math.PI,\n            size: 4 + Math.random() * 5,\n            color: shades[index % shades.length]!,\n          })\n        }\n\n        if (announce) setMessage(announce)\n        setAlive(true)\n      },\n    }),\n    [announce, count, velocity]\n  )\n\n  const setup = React.useCallback(() => pieces, [])\n\n  const draw = React.useCallback(\n    ({\n      context,\n      size,\n      delta,\n    }: {\n      context: CanvasRenderingContext2D\n      size: { width: number; height: number; dpr: number }\n      delta: number\n    }) => {\n      const { width, height, dpr } = size\n      context.setTransform(dpr, 0, 0, dpr, 0, 0)\n      context.clearRect(0, 0, width, height)\n\n      const living: Piece[] = []\n\n      for (const piece of pieces.current) {\n        piece.life -= delta\n        if (piece.life <= 0) continue\n\n        piece.vy += gravity * delta\n        piece.vx *= 1 - 1.2 * delta\n        piece.x += piece.vx * delta\n        piece.y += piece.vy * delta\n        piece.angle += piece.spin * delta\n\n        if (piece.y - piece.size > height) continue\n        living.push(piece)\n\n        const [r, g, b] = piece.color\n        context.save()\n        context.translate(piece.x, piece.y)\n        context.rotate(piece.angle)\n        context.globalAlpha = Math.min(1, piece.life * 1.6)\n        context.fillStyle = `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`\n        context.fillRect(\n          -piece.size / 2,\n          -piece.size / 4,\n          piece.size,\n          piece.size / 2\n        )\n        context.restore()\n      }\n\n      pieces.current = living\n      if (living.length === 0) setAlive(false)\n    },\n    [gravity]\n  )\n\n  return (\n    <div\n      ref={rootRef}\n      data-slot=\"burst\"\n      className={cn(\"pointer-events-none absolute inset-0 z-50\", className)}\n      {...rootProps}\n    >\n      <RenderSurface<React.RefObject<Piece[]>, \"2d\">\n        setup={setup}\n        draw={draw}\n        paused={!alive}\n        className=\"absolute inset-0\"\n      />\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {message}\n      </span>\n    </div>\n  )\n})\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "scene",
    "feedback",
    "celebration",
    "canvas"
  ],
  "type": "registry:ui"
}