{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sparkles","type":"registry:ui","title":"Sparkles","description":"Canvas-based floating particle sparkle effect with configurable density and colors","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["backgrounds"],"registryDependencies":["https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/sparkles/Sparkles.tsx","content":"\"use client\";\n\nimport { useEffect } from \"react\";\nimport { cn } from \"../../utils\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\n\n/**\n * Sparkles - canvas-based floating particle sparkle effect\n *\n * Particles drift across a percentage-space field (0-100 on both axes, wrapping\n * at -2/102) and pulse their alpha off a per-particle sine phase. Everything is\n * drawn on a 2D canvas from a `requestAnimationFrame` loop; the canvas is sized\n * from the container's `getBoundingClientRect()` scaled by `devicePixelRatio`\n * and kept in sync by a `ResizeObserver` on the container.\n *\n * The component fills its parent (`size-full`), so give the parent a height.\n */\nexport interface SparklesProps {\n\t/** Background color */\n\tbackground?: string;\n\t/** Particle color: a hex string, or `currentColor` to follow the text colour */\n\tparticleColor?: string;\n\t/** Minimum particle size */\n\tminSize?: number;\n\t/** Maximum particle size */\n\tmaxSize?: number;\n\t/** Particle movement speed */\n\tspeed?: number;\n\t/** Number of particles */\n\tparticleDensity?: number;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Seed for the particle field. The same seed always lays out and drifts the\n\t * same sparkles, which is what keeps the component free of `Math.random()`.\n\t * Change it to give two fields on one page different sparkles - the default\n\t * is shared, so two `<Sparkles />` with no seed show the same field.\n\t */\n\tseed?: number;\n}\n\ninterface Particle {\n\tx: number;\n\ty: number;\n\tsize: number;\n\topacity: number;\n\tvx: number;\n\tvy: number;\n\tphase: number;\n\tphaseSpeed: number;\n}\n\n/**\n * mulberry32 - a tiny deterministic PRNG, standing in for the source's bare\n * `Math.random()`. Same uniform distribution, so the field looks identical, but\n * a seeded stream buys a field that comes back the same after a remount and a\n * draw path a test can assert on frame by frame.\n */\nfunction mulberry32(seed: number): () => number {\n\tlet state = seed >>> 0;\n\treturn () => {\n\t\tstate = (state + 0x6d2b79f5) >>> 0;\n\t\tlet t = state;\n\t\tt = Math.imul(t ^ (t >>> 15), t | 1);\n\t\tt ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n\t\treturn ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n\t};\n}\n\nconst CURRENT_COLOR = /^\\s*currentcolor\\s*$/i;\n\n/**\n * Resolve `currentColor` to a `#rrggbb` string from `el`'s computed text colour\n * (the particle loop appends a hex alpha byte, so it needs hex). The common\n * `rgb()`/`rgba()` serialisation is parsed directly; newer spaces are\n * rasterised through a 1x1 canvas. Any other value is returned unchanged.\n */\nfunction resolveCurrentColor(color: string, el: Element): string {\n\tif (!CURRENT_COLOR.test(color)) return color;\n\tconst computed = getComputedStyle(el).color;\n\tlet r = 0;\n\tlet g = 0;\n\tlet b = 0;\n\tconst m = /^rgba?\\(\\s*([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/i.exec(computed);\n\tif (m) {\n\t\tr = Math.round(+m[1]!);\n\t\tg = Math.round(+m[2]!);\n\t\tb = Math.round(+m[3]!);\n\t} else if (computed) {\n\t\ttry {\n\t\t\tconst probe = document.createElement(\"canvas\");\n\t\t\tprobe.width = probe.height = 1;\n\t\t\tconst pctx = probe.getContext(\"2d\", { willReadFrequently: true });\n\t\t\tif (pctx) {\n\t\t\t\tpctx.fillStyle = computed;\n\t\t\t\tpctx.fillRect(0, 0, 1, 1);\n\t\t\t\t[r = 0, g = 0, b = 0] = pctx.getImageData(0, 0, 1, 1).data;\n\t\t\t}\n\t\t} catch {\n\t\t\t// keep the black fallback\n\t\t}\n\t}\n\treturn `#${[r, g, b].map((c) => c.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nexport function Sparkles({\n\tbackground = \"#0d47a1\",\n\tparticleColor = \"#ffffff\",\n\tminSize = 1,\n\tmaxSize = 3,\n\tspeed = 4,\n\tparticleDensity = 120,\n\tclassName = \"\",\n\tseed = 1,\n}: SparklesProps) {\n\tconst [container, containerRef] = useElementRef<HTMLDivElement>();\n\tconst [canvas, canvasRef] = useElementRef<HTMLCanvasElement>();\n\n\t// Read inside the running loop only. The source's `updateAndDraw` reads\n\t// `particleColor` through the rune getter on every frame, so a colour change\n\t// must repaint on the next frame rather than tear the field down and re-seed\n\t// it. The other five props are read synchronously by `generateParticles()`,\n\t// which is why they are effect dependencies below.\n\tconst particleColorRef = useLiveRef(particleColor);\n\n\tuseEffect(() => {\n\t\tif (!container || !canvas) return;\n\n\t\tconst random = mulberry32(seed);\n\n\t\tlet ctx: CanvasRenderingContext2D | null = null;\n\t\tlet particles: Particle[] = [];\n\t\tlet rafId = 0;\n\t\t// Kept alongside the backing store because the context transform below\n\t\t// puts the draw calls in CSS-pixel space while `canvas.width` counts\n\t\t// device pixels; the draw site divides by it to stay in the same space.\n\t\tlet dpr = 1;\n\n\t\tfunction resizeCanvas() {\n\t\t\tif (!canvas || !container) return;\n\t\t\tdpr = window.devicePixelRatio || 1;\n\t\t\tconst rect = container.getBoundingClientRect();\n\t\t\tcanvas.width = rect.width * dpr;\n\t\t\tcanvas.height = rect.height * dpr;\n\t\t\tctx = canvas.getContext(\"2d\");\n\t\t\tif (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\t\t}\n\n\t\tfunction generateParticles() {\n\t\t\tparticles = [];\n\t\t\tfor (let i = 0; i < particleDensity; i++) {\n\t\t\t\tconst baseSpeed = 0.05;\n\t\t\t\tconst speedVariance = random() * 0.3 + 0.7;\n\t\t\t\tparticles.push({\n\t\t\t\t\tx: random() * 100,\n\t\t\t\t\ty: random() * 100,\n\t\t\t\t\tsize: random() * (maxSize - minSize) + minSize,\n\t\t\t\t\topacity: random() * 0.5 + 0.3,\n\t\t\t\t\tvx: (random() - 0.5) * baseSpeed * speedVariance * speed,\n\t\t\t\t\tvy: ((random() - 0.5) * baseSpeed - baseSpeed * 0.3) * speedVariance * speed,\n\t\t\t\t\tphase: random() * Math.PI * 2,\n\t\t\t\t\tphaseSpeed: 0.015,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tfunction updateAndDraw() {\n\t\t\t// Local alias, because `ctx` is reassigned by `resizeCanvas` and\n\t\t\t// TypeScript will not carry a null check on an outer `let` into the\n\t\t\t// draw calls below. Same value, same guard as the source.\n\t\t\tconst context = ctx;\n\t\t\tif (!context || !canvas) return;\n\t\t\tcontext.clearRect(0, 0, canvas.width, canvas.height);\n\t\t\tconst fill = resolveCurrentColor(particleColorRef.current, canvas);\n\n\t\t\tfor (const p of particles) {\n\t\t\t\tp.x += p.vx;\n\t\t\t\tp.y += p.vy;\n\t\t\t\tif (p.x < -2) p.x = 102;\n\t\t\t\tif (p.x > 102) p.x = -2;\n\t\t\t\tif (p.y < -2) p.y = 102;\n\t\t\t\tif (p.y > 102) p.y = -2;\n\t\t\t\tp.phase = (p.phase + p.phaseSpeed) % (Math.PI * 2);\n\t\t\t\tconst opacity = 0.3 + (Math.sin(p.phase) * 0.3 + 0.3);\n\n\t\t\t\tcontext.beginPath();\n\t\t\t\tcontext.arc(\n\t\t\t\t\t(p.x / 100) * (canvas.width / dpr),\n\t\t\t\t\t(p.y / 100) * (canvas.height / dpr),\n\t\t\t\t\tp.size,\n\t\t\t\t\t0,\n\t\t\t\t\tMath.PI * 2\n\t\t\t\t);\n\t\t\t\tcontext.fillStyle = `${fill}${Math.floor(opacity * 255)\n\t\t\t\t\t.toString(16)\n\t\t\t\t\t.padStart(2, \"0\")}`;\n\t\t\t\tcontext.fill();\n\t\t\t}\n\n\t\t\trafId = requestAnimationFrame(updateAndDraw);\n\t\t}\n\n\t\tctx = canvas.getContext(\"2d\");\n\t\tresizeCanvas();\n\t\tgenerateParticles();\n\t\trafId = requestAnimationFrame(updateAndDraw);\n\n\t\tconst resizeObserver = new ResizeObserver(resizeCanvas);\n\t\tresizeObserver.observe(container);\n\n\t\treturn () => {\n\t\t\tcancelAnimationFrame(rafId);\n\t\t\tresizeObserver.disconnect();\n\t\t};\n\t}, [container, canvas, minSize, maxSize, speed, particleDensity, seed, particleColorRef]);\n\n\treturn (\n\t\t<div\n\t\t\tref={containerRef}\n\t\t\tclassName={cn(\"relative size-full overflow-hidden will-change-transform\", className)}\n\t\t\tstyle={{ background }}\n\t\t>\n\t\t\t<canvas ref={canvasRef} className=\"absolute inset-0 size-full\" aria-hidden=\"true\" />\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/sparkles/Sparkles.tsx"},{"path":"react/src/components/sparkles/index.ts","content":"\"use client\";\n\nexport { Sparkles } from \"./Sparkles\";\nexport type { SparklesProps } from \"./Sparkles\";\n","type":"registry:ui","target":"@ui/fancy/components/sparkles/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/sparkles?framework=react"}}