{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "confetti-background",
  "type": "registry:ui",
  "title": "Confetti Background",
  "description": "Falling confetti in your theme colors.",
  "files": [
    {
      "path": "src/registry/backgrounds/confetti-background.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ConfettiBackgroundProps = React.ComponentProps<\"div\"> & {\n  /** Piece colors. Any CSS colors, including theme vars. Defaults to the chart tokens. */\n  colors?: string[];\n  /** Multiplier for the number of pieces (capped by area). */\n  density?: number;\n  /** Fall speed multiplier. */\n  speed?: number;\n};\n\nconst DEFAULT_COLORS = [\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n  \"var(--chart-5)\",\n];\nconst MAX_PIECES = 180;\n\n/** Confetti pieces that tumble and flutter down endlessly. */\nexport function ConfettiBackground({\n  colors = DEFAULT_COLORS,\n  density = 1,\n  speed = 1,\n  className,\n  ...props\n}: ConfettiBackgroundProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  // Stable dependency for inline arrays.\n  const colorKey = colors.join(\"\\n\");\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!root || !canvas || !ctx) return;\n\n    const reducedMotion = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    const sourceColors = colorKey ? colorKey.split(\"\\n\") : [];\n    const fills: string[] = [];\n\n    let width = 0;\n    let height = 0;\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n\n    let count = 0;\n    let px = new Float32Array(0);\n    let py = new Float32Array(0);\n    let pSize = new Float32Array(0);\n    let pFall = new Float32Array(0);\n    let pAngle = new Float32Array(0);\n    let pSpin = new Float32Array(0);\n    let pFlip = new Float32Array(0);\n    let pFlipSpeed = new Float32Array(0);\n    let pSway = new Float32Array(0);\n\n    const resolveColors = () => {\n      fills.length = 0;\n      for (const value of sourceColors) {\n        canvas.style.color = value;\n        fills.push(getComputedStyle(canvas).color);\n      }\n      canvas.style.color = \"\";\n    };\n\n    const init = () => {\n      count = Math.min(\n        MAX_PIECES,\n        Math.round(((width * height) / 7000) * density)\n      );\n      px = new Float32Array(count);\n      py = new Float32Array(count);\n      pSize = new Float32Array(count);\n      pFall = new Float32Array(count);\n      pAngle = new Float32Array(count);\n      pSpin = new Float32Array(count);\n      pFlip = new Float32Array(count);\n      pFlipSpeed = new Float32Array(count);\n      pSway = new Float32Array(count);\n      for (let i = 0; i < count; i++) {\n        px[i] = Math.random() * width;\n        py[i] = Math.random() * height;\n        pSize[i] = 5 + Math.random() * 7;\n        pFall[i] = 40 + Math.random() * 70;\n        pAngle[i] = Math.random() * Math.PI * 2;\n        pSpin[i] = (Math.random() - 0.5) * 6;\n        pFlip[i] = Math.random() * Math.PI * 2;\n        pFlipSpeed[i] = 2 + Math.random() * 4;\n        pSway[i] = Math.random() * Math.PI * 2;\n      }\n    };\n\n    const step = (dt: number) => {\n      for (let i = 0; i < count; i++) {\n        py[i] += pFall[i] * speed * dt;\n        pSway[i] += dt * 1.5;\n        px[i] += Math.sin(pSway[i]) * 25 * dt;\n        pAngle[i] += pSpin[i] * dt;\n        pFlip[i] += pFlipSpeed[i] * dt;\n        if (py[i] > height + pSize[i]) {\n          py[i] = -pSize[i];\n          px[i] = Math.random() * width;\n        }\n      }\n    };\n\n    const draw = () => {\n      ctx.clearRect(0, 0, width, height);\n      const colorCount = fills.length;\n      if (!colorCount) return;\n      ctx.globalAlpha = 0.9;\n      // One path per color: piece i uses color i % colorCount.\n      for (let c = 0; c < colorCount; c++) {\n        ctx.fillStyle = fills[c];\n        ctx.beginPath();\n        for (let i = c; i < count; i += colorCount) {\n          // Rotated rectangle, squashed horizontally by the flip to fake a 3D tumble.\n          const hw = (pSize[i] / 2) * Math.abs(Math.cos(pFlip[i]));\n          const hh = pSize[i] * 0.3;\n          const cos = Math.cos(pAngle[i]);\n          const sin = Math.sin(pAngle[i]);\n          const x = px[i];\n          const y = py[i];\n          ctx.moveTo(x - hw * cos + hh * sin, y - hw * sin - hh * cos);\n          ctx.lineTo(x + hw * cos + hh * sin, y + hw * sin - hh * cos);\n          ctx.lineTo(x + hw * cos - hh * sin, y + hw * sin + hh * cos);\n          ctx.lineTo(x - hw * cos - hh * sin, y - hw * sin + hh * cos);\n          ctx.closePath();\n        }\n        ctx.fill();\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      width = root.clientWidth;\n      height = root.clientHeight;\n      canvas.width = Math.round(width * dpr);\n      canvas.height = Math.round(height * dpr);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      if (!width || !height) return;\n      init();\n      draw();\n    };\n\n    const frame = (now: number) => {\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;\n      last = now;\n      step(dt);\n      draw();\n      raf = requestAnimationFrame(frame);\n    };\n\n    const start = () => {\n      if (reducedMotion || raf || !visible || !width || !height) return;\n      last = 0;\n      raf = requestAnimationFrame(frame);\n    };\n\n    const stop = () => {\n      cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    resolveColors();\n\n    const resizeObserver = new ResizeObserver(() => {\n      resize();\n      start();\n    });\n    resizeObserver.observe(root);\n\n    const intersectionObserver = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) start();\n      else stop();\n    });\n    intersectionObserver.observe(root);\n\n    const themeObserver = new MutationObserver(() => {\n      resolveColors();\n      if (!raf && width && height) draw();\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\", \"style\", \"data-theme\"],\n    });\n\n    return () => {\n      stop();\n      resizeObserver.disconnect();\n      intersectionObserver.disconnect();\n      themeObserver.disconnect();\n    };\n  }, [colorKey, density, speed]);\n\n  return (\n    <div\n      ref={rootRef}\n      aria-hidden\n      data-slot=\"confetti-background\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 overflow-hidden\",\n        className\n      )}\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"absolute inset-0 size-full\" />\n    </div>\n  );\n}\n"
    }
  ]
}
