{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-confetti", "description": "A hook to trigger confetti animation", "dependencies": [], "devDependencies": [], "registryDependencies": [], "files": [ { "path": "packages/hooks/use-confetti/index.tsx", "content": "import { useCallback, useEffect, useRef } from \"react\";\n\nexport interface ConfettiOptions {\n\tparticleCount?: number;\n\torigin?:\n\t\t| \"top\"\n\t\t| \"bottom\"\n\t\t| \"left\"\n\t\t| \"right\"\n\t\t| \"center\"\n\t\t| \"top-left\"\n\t\t| \"top-right\"\n\t\t| \"bottom-left\"\n\t\t| \"bottom-right\";\n\tduration?: number;\n}\n\ninterface Particle {\n\tupdate: () => void;\n\trender: (ctx: CanvasRenderingContext2D) => void;\n\tposition: { x: number; y: number };\n}\n\nconst gravityConfetti = 0.3;\nconst gravitySequins = 0.55;\nconst dragConfetti = 0.075;\nconst dragSequins = 0.02;\nconst terminalVelocity = 3;\n\nconst randomRange = (min: number, max: number) =>\n\tMath.random() * (max - min) + min;\n\nconst randomColor = () => {\n\tconst r = Math.floor(Math.random() * 256);\n\tconst g = Math.floor(Math.random() * 256);\n\tconst b = Math.floor(Math.random() * 256);\n\treturn `rgb(${r},${g},${b})`;\n};\n\nconst initConfettoVelocity = (\n\txRange: [number, number],\n\tyRange: [number, number],\n) => {\n\tconst x = randomRange(xRange[0], xRange[1]);\n\tconst range = yRange[1] - yRange[0] + 1;\n\tlet y =\n\t\tyRange[1] -\n\t\tMath.abs(randomRange(0, range) + randomRange(0, range) - range);\n\tif (y >= yRange[1] - 1) {\n\t\ty += Math.random() < 0.25 ? randomRange(1, 3) : 0;\n\t}\n\treturn { x, y: -y };\n};\n\nfunction calculateOrigin(\n\tcanvasWidth: number,\n\tcanvasHeight: number,\n\torigin?: ConfettiOptions[\"origin\"],\n) {\n\tconst originMap = {\n\t\tcenter: {\n\t\t\tx: [canvasWidth / 2 - 50, canvasWidth / 2 + 50],\n\t\t\ty: [canvasHeight / 2 + 50, canvasHeight / 2 + 100],\n\t\t},\n\t\ttop: { x: [canvasWidth / 2 - 50, canvasWidth / 2 + 50], y: [50, 100] },\n\t\tbottom: {\n\t\t\tx: [canvasWidth / 2 - 50, canvasWidth / 2 + 50],\n\t\t\ty: [canvasHeight - 100, canvasHeight - 50],\n\t\t},\n\t\tleft: {\n\t\t\tx: [50, 100],\n\t\t\ty: [canvasHeight / 2 - 50, canvasHeight / 2 + 50],\n\t\t},\n\t\tright: {\n\t\t\tx: [canvasWidth - 100, canvasWidth - 50],\n\t\t\ty: [canvasHeight / 2 - 50, canvasHeight / 2 + 50],\n\t\t},\n\t\t\"top-left\": { x: [50, 100], y: [50, 100] },\n\t\t\"top-right\": { x: [canvasWidth - 100, canvasWidth - 50], y: [50, 100] },\n\t\t\"bottom-left\": {\n\t\t\tx: [50, 100],\n\t\t\ty: [canvasHeight - 100, canvasHeight - 50],\n\t\t},\n\t\t\"bottom-right\": {\n\t\t\tx: [canvasWidth - 100, canvasWidth - 50],\n\t\t\ty: [canvasHeight - 100, canvasHeight - 50],\n\t\t},\n\t};\n\n\treturn originMap[origin || \"center\"];\n}\n\nfunction createConfetto(\n\tcanvasWidth: number,\n\tcanvasHeight: number,\n\torigin?: ConfettiOptions[\"origin\"],\n): Particle {\n\tconst originBounds = calculateOrigin(canvasWidth, canvasHeight, origin);\n\tconst randomModifier = randomRange(0, 99);\n\tconst color = { front: randomColor(), back: randomColor() };\n\n\tconst dimensions = { x: randomRange(5, 9), y: randomRange(8, 15) };\n\tconst position = {\n\t\tx: randomRange(originBounds.x[0], originBounds.x[1]),\n\t\ty: randomRange(originBounds.y[0], originBounds.y[1]),\n\t};\n\n\tconst rotation = randomRange(0, 2 * Math.PI);\n\tconst scale = { x: 1, y: 1 };\n\tconst velocity = initConfettoVelocity([-9, 9], [6, 11]);\n\n\treturn {\n\t\tupdate() {\n\t\t\tvelocity.x -= velocity.x * dragConfetti;\n\t\t\tvelocity.y = Math.min(\n\t\t\t\tvelocity.y + gravityConfetti,\n\t\t\t\tterminalVelocity,\n\t\t\t);\n\t\t\tvelocity.x += Math.random() > 0.5 ? Math.random() : -Math.random();\n\n\t\t\tposition.x += velocity.x;\n\t\t\tposition.y += velocity.y;\n\n\t\t\tscale.y = Math.cos((position.y + randomModifier) * 0.09);\n\t\t},\n\t\trender(ctx) {\n\t\t\tconst width = dimensions.x * scale.x;\n\t\t\tconst height = dimensions.y * scale.y;\n\n\t\t\tctx.save();\n\t\t\tctx.translate(position.x, position.y);\n\t\t\tctx.rotate(rotation);\n\n\t\t\tctx.fillStyle = scale.y > 0 ? color.front : color.back;\n\t\t\tctx.fillRect(-width / 2, -height / 2, width, height);\n\n\t\t\tctx.restore();\n\t\t},\n\t\tposition,\n\t};\n}\n\nfunction createSequin(\n\tcanvasWidth: number,\n\tcanvasHeight: number,\n\torigin?: ConfettiOptions[\"origin\"],\n): Particle {\n\tconst originBounds = calculateOrigin(canvasWidth, canvasHeight, origin);\n\tconst color = randomColor();\n\tconst radius = randomRange(1, 2);\n\n\tconst position = {\n\t\tx: randomRange(originBounds.x[0], originBounds.x[1]),\n\t\ty: randomRange(originBounds.y[0], originBounds.y[1]),\n\t};\n\tconst velocity = { x: randomRange(-6, 6), y: randomRange(-8, -12) };\n\n\treturn {\n\t\tupdate() {\n\t\t\tvelocity.x -= velocity.x * dragSequins;\n\t\t\tvelocity.y += gravitySequins;\n\n\t\t\tposition.x += velocity.x;\n\t\t\tposition.y += velocity.y;\n\t\t},\n\t\trender(ctx) {\n\t\t\tctx.save();\n\t\t\tctx.translate(position.x, position.y);\n\t\t\tctx.fillStyle = color;\n\t\t\tctx.beginPath();\n\t\t\tctx.arc(0, 0, radius, 0, 2 * Math.PI);\n\t\t\tctx.fill();\n\t\t\tctx.restore();\n\t\t},\n\t\tposition,\n\t};\n}\n\n/* -------------------------------------------------- */\n/* HOOK START */\n/* -------------------------------------------------- */\n\nexport function useConfetti() {\n\tconst canvasRef = useRef(null);\n\tconst ctxRef = useRef(null);\n\tconst particlesRef = useRef([]);\n\tconst frameRef = useRef(null);\n\n\tconst createCanvas = () => {\n\t\tif (typeof window === \"undefined\") return;\n\n\t\tconst canvas = document.createElement(\"canvas\");\n\t\tcanvas.style.position = \"fixed\";\n\t\tcanvas.style.top = \"0\";\n\t\tcanvas.style.left = \"0\";\n\t\tcanvas.style.pointerEvents = \"none\";\n\t\tcanvas.style.zIndex = \"9999\";\n\n\t\tdocument.body.appendChild(canvas);\n\t\tcanvasRef.current = canvas;\n\t\tctxRef.current = canvas.getContext(\"2d\");\n\n\t\tresizeCanvas();\n\n\t\twindow.addEventListener(\"resize\", resizeCanvas);\n\t};\n\n\tconst resizeCanvas = () => {\n\t\tconst canvas = canvasRef.current;\n\t\tif (!canvas) return;\n\n\t\tcanvas.width = window.innerWidth;\n\t\tcanvas.height = window.innerHeight;\n\t};\n\n\tconst removeCanvas = () => {\n\t\tconst canvas = canvasRef.current;\n\t\tif (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);\n\n\t\tif (frameRef.current) cancelAnimationFrame(frameRef.current);\n\n\t\tcanvasRef.current = null;\n\t\tctxRef.current = null;\n\t\tparticlesRef.current = [];\n\t\tframeRef.current = null;\n\t};\n\n\tconst render = () => {\n\t\tconst canvas = canvasRef.current;\n\t\tconst ctx = ctxRef.current;\n\t\tif (!canvas || !ctx) return;\n\n\t\tctx.clearRect(0, 0, canvas.width, canvas.height);\n\n\t\tparticlesRef.current.forEach((p) => {\n\t\t\tp.update();\n\t\t\tp.render(ctx);\n\t\t});\n\n\t\tparticlesRef.current = particlesRef.current.filter(\n\t\t\t(p) => p.position.y < canvas.height,\n\t\t);\n\n\t\tif (particlesRef.current.length > 0) {\n\t\t\tframeRef.current = requestAnimationFrame(render);\n\t\t} else {\n\t\t\tremoveCanvas();\n\t\t}\n\t};\n\n\tconst triggerConfetti = useCallback((options: ConfettiOptions = {}) => {\n\t\tconst {\n\t\t\tparticleCount = 50,\n\t\t\torigin = \"center\",\n\t\t\tduration = 3000,\n\t\t} = options;\n\n\t\tif (!canvasRef.current) createCanvas();\n\t\tif (!canvasRef.current || !ctxRef.current) return;\n\n\t\tconst canvas = canvasRef.current;\n\n\t\tconst confettiCount = Math.floor(particleCount * 0.75);\n\t\tconst sequinCount = Math.floor(particleCount * 0.25);\n\n\t\tconst newConfetti = Array.from({ length: confettiCount }, () =>\n\t\t\tcreateConfetto(canvas.width, canvas.height, origin),\n\t\t);\n\t\tconst newSequins = Array.from({ length: sequinCount }, () =>\n\t\t\tcreateSequin(canvas.width, canvas.height, origin),\n\t\t);\n\n\t\tparticlesRef.current.push(...newConfetti, ...newSequins);\n\n\t\tif (!frameRef.current) {\n\t\t\tframeRef.current = requestAnimationFrame(render);\n\t\t}\n\n\t\tsetTimeout(() => {\n\t\t\tif (particlesRef.current.length === 0) removeCanvas();\n\t\t}, duration);\n\t}, []);\n\n\t// Cleanup if component unmounts\n\tuseEffect(() => {\n\t\treturn () => removeCanvas();\n\t}, []);\n\n\treturn triggerConfetti;\n}\n", "type": "registry:hook", "target": "hooks/use-confetti.tsx" } ], "type": "registry:hook" }