11'use client' ;
22
3- import confetti from 'canvas-confetti ' ;
4- import { useCallback } from 'react ' ;
3+ import { useCallback , useState } from 'react ' ;
4+ import { motion , AnimatePresence } from 'framer-motion ' ;
55
66const BRAND_COLORS = [ '#6366F1' , '#8B5CF6' , '#D946EF' , '#F5A623' , '#FFD700' , '#2DDF8E' ] ;
77
@@ -10,76 +10,102 @@ function prefersReducedMotion() {
1010 return window . matchMedia ( '(prefers-reduced-motion: reduce)' ) . matches ;
1111}
1212
13- /**
14- * Fire a confetti burst from the center of the screen.
15- */
16- export function fireConfetti ( ) {
17- if ( prefersReducedMotion ( ) ) return ;
18- confetti ( {
19- particleCount : 80 ,
20- spread : 70 ,
21- origin : { y : 0.6 } ,
22- colors : BRAND_COLORS ,
23- } ) ;
13+ function randomBetween ( min , max ) {
14+ return Math . random ( ) * ( max - min ) + min ;
2415}
2516
26- /**
27- * Fire confetti from a specific DOM element's position.
28- */
29- export function fireConfettiFromElement ( element ) {
30- if ( prefersReducedMotion ( ) || ! element ) return ;
31- const rect = element . getBoundingClientRect ( ) ;
32- const x = ( rect . left + rect . width / 2 ) / window . innerWidth ;
33- const y = ( rect . top + rect . height / 2 ) / window . innerHeight ;
34- confetti ( {
35- particleCount : 60 ,
36- spread : 55 ,
37- origin : { x, y } ,
38- colors : BRAND_COLORS ,
39- } ) ;
17+ function createParticles ( count = 40 , origin = { x : 0.5 , y : 0.6 } ) {
18+ const w = typeof window !== 'undefined' ? window . innerWidth : 800 ;
19+ const h = typeof window !== 'undefined' ? window . innerHeight : 600 ;
20+ return Array . from ( { length : count } , ( _ , i ) => ( {
21+ id : `${ Date . now ( ) } -${ i } ` ,
22+ x : origin . x * w ,
23+ y : origin . y * h ,
24+ color : BRAND_COLORS [ Math . floor ( Math . random ( ) * BRAND_COLORS . length ) ] ,
25+ size : randomBetween ( 6 , 12 ) ,
26+ angle : randomBetween ( 0 , 360 ) ,
27+ velocity : randomBetween ( 200 , 500 ) ,
28+ rotation : randomBetween ( - 180 , 180 ) ,
29+ } ) ) ;
4030}
4131
42- /**
43- * Fire confetti from both sides of the screen.
44- */
45- export function fireSideConfetti ( ) {
46- if ( prefersReducedMotion ( ) ) return ;
47- const defaults = { colors : BRAND_COLORS , startVelocity : 30 , spread : 360 , ticks : 60 , zIndex : 9999 } ;
48- confetti ( { ...defaults , particleCount : 40 , origin : { x : 0 , y : 0.5 } , angle : 60 } ) ;
49- confetti ( { ...defaults , particleCount : 40 , origin : { x : 1 , y : 0.5 } , angle : 120 } ) ;
32+ function Particle ( { particle } ) {
33+ const rad = ( particle . angle * Math . PI ) / 180 ;
34+ const dx = Math . cos ( rad ) * particle . velocity ;
35+ const dy = Math . sin ( rad ) * particle . velocity - 200 ;
36+
37+ return (
38+ < motion . div
39+ initial = { {
40+ x : particle . x ,
41+ y : particle . y ,
42+ scale : 1 ,
43+ rotate : 0 ,
44+ opacity : 1 ,
45+ } }
46+ animate = { {
47+ x : particle . x + dx ,
48+ y : particle . y + dy + 400 ,
49+ scale : 0 ,
50+ rotate : particle . rotation ,
51+ opacity : 0 ,
52+ } }
53+ transition = { { duration : 1.5 , ease : [ 0.22 , 1 , 0.36 , 1 ] } }
54+ style = { {
55+ position : 'fixed' ,
56+ width : particle . size ,
57+ height : particle . size ,
58+ borderRadius : particle . size > 9 ? '2px' : '50%' ,
59+ backgroundColor : particle . color ,
60+ pointerEvents : 'none' ,
61+ zIndex : 9999 ,
62+ } }
63+ />
64+ ) ;
5065}
5166
52- /**
53- * Fire star-shaped confetti.
54- */
55- export function fireStars ( ) {
56- if ( prefersReducedMotion ( ) ) return ;
57- const defaults = { spread : 360 , ticks : 80 , decay : 0.94 , startVelocity : 20 , zIndex : 9999 , colors : BRAND_COLORS } ;
58- confetti ( { ...defaults , particleCount : 30 , shapes : [ 'star' ] , scalar : 1.2 } ) ;
59- setTimeout ( ( ) => {
60- confetti ( { ...defaults , particleCount : 20 , shapes : [ 'star' ] , scalar : 0.8 } ) ;
61- } , 150 ) ;
67+ function ConfettiExplosion ( { particles } ) {
68+ if ( ! particles || particles . length === 0 ) return null ;
69+ return (
70+ < div style = { { position : 'fixed' , inset : 0 , pointerEvents : 'none' , zIndex : 9999 } } >
71+ < AnimatePresence >
72+ { particles . map ( ( p ) => (
73+ < Particle key = { p . id } particle = { p } />
74+ ) ) }
75+ </ AnimatePresence >
76+ </ div >
77+ ) ;
6278}
6379
6480/**
6581 * React hook for triggering confetti from a component.
66- * Returns { fire, ConfettiComponent } — ConfettiComponent is null (canvas-confetti manages its own canvas) .
82+ * Returns { fire, ConfettiComponent }.
6783 */
6884export function useConfetti ( ) {
85+ const [ particles , setParticles ] = useState ( [ ] ) ;
86+
6987 const fire = useCallback ( ( opts = { } ) => {
7088 if ( prefersReducedMotion ( ) ) return ;
71- const { x , y } = opts ;
72- const origin = { } ;
73- if ( x !== undefined ) origin . x = typeof x === 'string' ? 0.5 : x / window . innerWidth ;
74- if ( y !== undefined ) origin . y = typeof y === 'string' ? 0.5 : y / window . innerHeight ;
75- confetti ( {
76- particleCount : 80 ,
77- spread : 70 ,
78- origin : { y : 0.6 , ... origin } ,
79- colors : BRAND_COLORS ,
80- } ) ;
89+ const w = typeof window !== 'undefined' ? window . innerWidth : 800 ;
90+ const h = typeof window !== 'undefined' ? window . innerHeight : 600 ;
91+ const origin = {
92+ x : opts . x !== undefined ? opts . x / w : 0.5 ,
93+ y : opts . y !== undefined ? opts . y / h : 0.6 ,
94+ } ;
95+ const newParticles = createParticles ( opts . count || 40 , origin ) ;
96+ setParticles ( newParticles ) ;
97+ const timer = setTimeout ( ( ) => setParticles ( [ ] ) , 2000 ) ;
98+ return ( ) => clearTimeout ( timer ) ;
8199 } , [ ] ) ;
82100
83- // No component needed — canvas-confetti creates its own canvas
84- return { fire, ConfettiComponent : null } ;
101+ const ConfettiComponent = < ConfettiExplosion particles = { particles } /> ;
102+
103+ return { fire, ConfettiComponent } ;
85104}
105+
106+ // Legacy named exports — these are no-ops since the hook-based system
107+ // manages its own rendering. Use useConfetti() in components instead.
108+ export function fireConfetti ( ) { }
109+ export function fireConfettiFromElement ( ) { }
110+ export function fireSideConfetti ( ) { }
111+ export function fireStars ( ) { }
0 commit comments