festive-effects 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2009 @@
1
+ import { jsx, Fragment } from 'react/jsx-runtime';
2
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
+
5
+ // Core types for Festive Effects library
6
+ /**
7
+ * Array of all valid festival types for validation
8
+ */
9
+ const FESTIVAL_TYPES = [
10
+ 'christmas',
11
+ 'newyear',
12
+ 'valentine',
13
+ 'easter',
14
+ 'halloween',
15
+ 'thanksgiving',
16
+ 'diwali',
17
+ 'chinesenewyear',
18
+ 'holi',
19
+ 'eid',
20
+ 'stpatricks',
21
+ 'independence',
22
+ ];
23
+
24
+ // Configuration for Festive Effects library
25
+ /**
26
+ * Intensity configuration for particle effects
27
+ * Defines particle multipliers and max counts for each intensity level
28
+ */
29
+ const INTENSITY_CONFIG = {
30
+ low: { particleMultiplier: 0.5, maxParticles: 30 },
31
+ medium: { particleMultiplier: 1.0, maxParticles: 60 },
32
+ high: { particleMultiplier: 2.0, maxParticles: 120 },
33
+ };
34
+ /**
35
+ * Calculate the actual particle count based on base count and intensity
36
+ * @param baseCount - The base particle count from festival config
37
+ * @param intensity - The intensity level
38
+ * @returns The calculated particle count, capped at maxParticles
39
+ */
40
+ function getParticleCount(baseCount, intensity = 'medium') {
41
+ const settings = INTENSITY_CONFIG[intensity];
42
+ const calculated = Math.floor(baseCount * settings.particleMultiplier);
43
+ return Math.min(calculated, settings.maxParticles);
44
+ }
45
+ /**
46
+ * Festival configurations for all 12 supported festivals
47
+ * Each config defines colors, particle types, animation style, and physics
48
+ */
49
+ const FESTIVAL_CONFIGS = {
50
+ christmas: {
51
+ baseParticleCount: 50,
52
+ colors: ['#FFFFFF', '#E8F4FF', '#B8D4E8', '#87CEEB'],
53
+ particleTypes: ['snowflake'],
54
+ animationType: 'fall',
55
+ physics: { speed: 3, drift: 50, rotation: 360, scale: [0.5, 1.5] },
56
+ },
57
+ newyear: {
58
+ baseParticleCount: 40,
59
+ colors: ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'],
60
+ particleTypes: ['firework', 'confetti', 'spark'],
61
+ animationType: 'explode',
62
+ physics: { speed: 5, drift: 100, rotation: 720, scale: [0.3, 1.2] },
63
+ },
64
+ valentine: {
65
+ baseParticleCount: 35,
66
+ colors: ['#FF6B6B', '#EE5A5A', '#FF8E8E', '#FFB6C1', '#FF69B4'],
67
+ particleTypes: ['heart'],
68
+ animationType: 'rise',
69
+ physics: { speed: 2, drift: 30, rotation: 15, scale: [0.6, 1.4] },
70
+ },
71
+ easter: {
72
+ baseParticleCount: 30,
73
+ colors: ['#FFB6C1', '#98FB98', '#87CEEB', '#DDA0DD', '#F0E68C'],
74
+ particleTypes: ['egg', 'flower'],
75
+ animationType: 'float',
76
+ physics: { speed: 1.5, drift: 20, rotation: 10, scale: [0.7, 1.3] },
77
+ },
78
+ halloween: {
79
+ baseParticleCount: 35,
80
+ colors: ['#FF6600', '#8B008B', '#000000', '#FFD700', '#32CD32'],
81
+ particleTypes: ['bat', 'pumpkin', 'spider'],
82
+ animationType: 'float',
83
+ physics: { speed: 2, drift: 60, rotation: 30, scale: [0.5, 1.5] },
84
+ },
85
+ thanksgiving: {
86
+ baseParticleCount: 40,
87
+ colors: ['#D2691E', '#FF8C00', '#B8860B', '#CD853F', '#8B4513'],
88
+ particleTypes: ['leaf'],
89
+ animationType: 'fall',
90
+ physics: { speed: 2, drift: 80, rotation: 540, scale: [0.6, 1.4] },
91
+ },
92
+ diwali: {
93
+ baseParticleCount: 45,
94
+ colors: ['#FFD700', '#FF8C00', '#FF4500', '#FFA500', '#FFFF00'],
95
+ particleTypes: ['diya', 'spark', 'rangoli'],
96
+ animationType: 'float',
97
+ physics: { speed: 1, drift: 15, rotation: 5, scale: [0.5, 1.2] },
98
+ },
99
+ chinesenewyear: {
100
+ baseParticleCount: 40,
101
+ colors: ['#FF0000', '#FFD700', '#FF4500', '#DC143C'],
102
+ particleTypes: ['lantern', 'firework', 'dragon'],
103
+ animationType: 'rise',
104
+ physics: { speed: 1.5, drift: 25, rotation: 10, scale: [0.6, 1.3] },
105
+ },
106
+ holi: {
107
+ baseParticleCount: 50,
108
+ colors: ['#FF1493', '#00FF00', '#FFFF00', '#FF4500', '#9400D3', '#00BFFF'],
109
+ particleTypes: ['color-splash'],
110
+ animationType: 'scatter',
111
+ physics: { speed: 4, drift: 150, rotation: 180, scale: [0.4, 2.0] },
112
+ },
113
+ eid: {
114
+ baseParticleCount: 35,
115
+ colors: ['#FFD700', '#C0C0C0', '#228B22', '#FFFFFF'],
116
+ particleTypes: ['moon', 'star'],
117
+ animationType: 'float',
118
+ physics: { speed: 0.8, drift: 10, rotation: 5, scale: [0.5, 1.5] },
119
+ },
120
+ stpatricks: {
121
+ baseParticleCount: 40,
122
+ colors: ['#228B22', '#32CD32', '#00FF00', '#FFD700'],
123
+ particleTypes: ['shamrock', 'rainbow'],
124
+ animationType: 'fall',
125
+ physics: { speed: 2, drift: 40, rotation: 180, scale: [0.6, 1.3] },
126
+ },
127
+ independence: {
128
+ baseParticleCount: 45,
129
+ colors: ['#FF0000', '#FFFFFF', '#0000FF'], // Default US colors, customizable
130
+ particleTypes: ['firework', 'spark', 'confetti'],
131
+ animationType: 'explode',
132
+ physics: { speed: 5, drift: 120, rotation: 360, scale: [0.3, 1.5] },
133
+ },
134
+ };
135
+
136
+ // Custom hooks for Festive Effects
137
+ // Contains accessibility and utility hooks
138
+ /**
139
+ * Hook to detect if the user prefers reduced motion
140
+ * Listens to the prefers-reduced-motion media query
141
+ *
142
+ * @returns boolean - true if user prefers reduced motion, false otherwise
143
+ *
144
+ * @example
145
+ * ```tsx
146
+ * const prefersReducedMotion = useReducedMotion();
147
+ * if (prefersReducedMotion) {
148
+ * // Don't render animations
149
+ * return null;
150
+ * }
151
+ * ```
152
+ */
153
+ function useReducedMotion() {
154
+ // Default to false for SSR
155
+ const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
156
+ useEffect(() => {
157
+ // Check if window is available (client-side)
158
+ if (typeof window === 'undefined') {
159
+ return;
160
+ }
161
+ // Create media query for prefers-reduced-motion
162
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
163
+ // Set initial value
164
+ setPrefersReducedMotion(mediaQuery.matches);
165
+ // Handler for media query changes
166
+ const handleChange = (event) => {
167
+ setPrefersReducedMotion(event.matches);
168
+ };
169
+ // Add listener for changes
170
+ // Use addEventListener for modern browsers, addListener for older ones
171
+ if (mediaQuery.addEventListener) {
172
+ mediaQuery.addEventListener('change', handleChange);
173
+ }
174
+ else {
175
+ // Fallback for older browsers
176
+ mediaQuery.addListener(handleChange);
177
+ }
178
+ // Cleanup listener on unmount
179
+ return () => {
180
+ if (mediaQuery.removeEventListener) {
181
+ mediaQuery.removeEventListener('change', handleChange);
182
+ }
183
+ else {
184
+ // Fallback for older browsers
185
+ mediaQuery.removeListener(handleChange);
186
+ }
187
+ };
188
+ }, []);
189
+ return prefersReducedMotion;
190
+ }
191
+ /**
192
+ * Hook to detect document visibility changes
193
+ * Returns true when the document is visible, false when hidden
194
+ *
195
+ * This is useful for pausing animations when the user switches tabs
196
+ * to save resources and improve performance.
197
+ *
198
+ * @returns boolean - true if document is visible, false if hidden
199
+ *
200
+ * @example
201
+ * ```tsx
202
+ * const isVisible = useDocumentVisibility();
203
+ * if (!isVisible) {
204
+ * // Pause animations
205
+ * }
206
+ * ```
207
+ */
208
+ function useDocumentVisibility() {
209
+ // Default to true for SSR (assume visible)
210
+ const [isVisible, setIsVisible] = useState(true);
211
+ useEffect(() => {
212
+ // Check if document is available (client-side)
213
+ if (typeof document === 'undefined') {
214
+ return;
215
+ }
216
+ // Set initial value based on current visibility state
217
+ setIsVisible(!document.hidden);
218
+ // Handler for visibility changes
219
+ const handleVisibilityChange = () => {
220
+ setIsVisible(!document.hidden);
221
+ };
222
+ // Add listener for visibility changes
223
+ document.addEventListener('visibilitychange', handleVisibilityChange);
224
+ // Cleanup listener on unmount
225
+ return () => {
226
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
227
+ };
228
+ }, []);
229
+ return isVisible;
230
+ }
231
+
232
+ // SVG Particle System for Festive Effects
233
+ // Contains all particle SVG shapes and the Particle component
234
+ /**
235
+ * SVG definitions for all 18 particle types
236
+ * Each SVG is designed to work with currentColor for dynamic coloring
237
+ */
238
+ const PARTICLE_SVGS = {
239
+ snowflake: `<svg viewBox="0 0 24 24"><path d="M12 0L12 24M0 12L24 12M4 4L20 20M20 4L4 20" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
240
+ heart: `<svg viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="currentColor"/></svg>`,
241
+ bat: `<svg viewBox="0 0 24 24"><path d="M12 4C8 4 4 8 2 12c2-2 4-2 6 0-1 2-1 4 0 6 1-2 3-3 4-3s3 1 4 3c1-2 1-4 0-6 2-2 4-2 6 0-2-4-6-8-10-8z" fill="currentColor"/></svg>`,
242
+ pumpkin: `<svg viewBox="0 0 24 24"><ellipse cx="12" cy="14" rx="8" ry="7" fill="currentColor"/><path d="M12 7V4M10 4h4" stroke="currentColor" stroke-width="2" fill="none"/></svg>`,
243
+ leaf: `<svg viewBox="0 0 24 24"><path d="M12 2C6 8 4 14 4 18c0 2 2 4 8 4s8-2 8-4c0-4-2-10-8-16z" fill="currentColor"/><path d="M12 22V8" stroke="rgba(0,0,0,0.3)" stroke-width="1" fill="none"/></svg>`,
244
+ diya: `<svg viewBox="0 0 24 24"><ellipse cx="12" cy="18" rx="8" ry="4" fill="currentColor"/><path d="M12 14c-2 0-3-2-3-4s1-4 3-4 3 2 3 4-1 4-3 4z" fill="#FFD700"/></svg>`,
245
+ lantern: `<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="14" rx="2" fill="currentColor"/><path d="M8 4h8M12 4V2" stroke="currentColor" stroke-width="2" fill="none"/></svg>`,
246
+ moon: `<svg viewBox="0 0 24 24"><path d="M12 3a9 9 0 109 9c0-5-4-9-9-9z" fill="currentColor"/></svg>`,
247
+ star: `<svg viewBox="0 0 24 24"><path d="M12 2l3 7h7l-5.5 4.5 2 7L12 16l-6.5 4.5 2-7L2 9h7z" fill="currentColor"/></svg>`,
248
+ shamrock: `<svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4" fill="currentColor"/><circle cx="7" cy="14" r="4" fill="currentColor"/><circle cx="17" cy="14" r="4" fill="currentColor"/><path d="M12 16v6" stroke="currentColor" stroke-width="2" fill="none"/></svg>`,
249
+ egg: `<svg viewBox="0 0 24 24"><ellipse cx="12" cy="14" rx="7" ry="9" fill="currentColor"/></svg>`,
250
+ flower: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3" fill="#FFD700"/><circle cx="12" cy="6" r="3" fill="currentColor"/><circle cx="18" cy="12" r="3" fill="currentColor"/><circle cx="12" cy="18" r="3" fill="currentColor"/><circle cx="6" cy="12" r="3" fill="currentColor"/></svg>`,
251
+ confetti: `<svg viewBox="0 0 24 24"><rect x="8" y="4" width="8" height="16" rx="1" fill="currentColor"/></svg>`,
252
+ spark: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" fill="currentColor"/></svg>`,
253
+ firework: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2" fill="currentColor"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4M4 4l3 3M17 17l3 3M4 20l3-3M17 7l3-3" stroke="currentColor" stroke-width="2" fill="none"/></svg>`,
254
+ 'color-splash': `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.8"/></svg>`,
255
+ spider: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="4" fill="currentColor"/><path d="M8 8L4 4M16 8L20 4M8 16L4 20M16 16L20 20M6 12H2M18 12h4" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`,
256
+ rainbow: `<svg viewBox="0 0 24 24"><path d="M2 20a10 10 0 0120 0" stroke="currentColor" stroke-width="4" fill="none"/></svg>`,
257
+ dragon: `<svg viewBox="0 0 24 24"><path d="M4 12c2-4 6-6 10-4 2 1 4 4 6 4-1 2-3 4-6 4-4 0-8-2-10-4z" fill="currentColor"/></svg>`,
258
+ rangoli: `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="12" cy="12" r="4" fill="currentColor"/></svg>`,
259
+ };
260
+ /**
261
+ * Get animation variants based on animation type
262
+ */
263
+ function getAnimationProps(particle, animationType, physics) {
264
+ const { delay, duration, custom } = particle;
265
+ const driftAmount = custom.drift ?? physics.drift * (Math.random() - 0.5) * 2;
266
+ const rotationAmount = custom.rotation ?? physics.rotation * (Math.random() - 0.5) * 2;
267
+ switch (animationType) {
268
+ case 'fall':
269
+ return {
270
+ initial: { y: -50, x: particle.x, opacity: 0, rotate: 0 },
271
+ animate: {
272
+ y: typeof window !== 'undefined' ? window.innerHeight + 50 : 1000,
273
+ x: particle.x + driftAmount,
274
+ opacity: [0, 1, 1, 0],
275
+ rotate: rotationAmount,
276
+ },
277
+ transition: {
278
+ duration,
279
+ ease: 'linear',
280
+ repeat: Infinity,
281
+ delay,
282
+ },
283
+ };
284
+ case 'rise':
285
+ return {
286
+ initial: { y: typeof window !== 'undefined' ? window.innerHeight + 50 : 1000, x: particle.x, opacity: 0, scale: 0 },
287
+ animate: {
288
+ y: -50,
289
+ x: [particle.x, particle.x + driftAmount / 2, particle.x - driftAmount / 2, particle.x],
290
+ opacity: [0, 1, 1, 0],
291
+ scale: [0, 1, 1.2, 1, 0],
292
+ },
293
+ transition: {
294
+ duration,
295
+ ease: 'easeOut',
296
+ repeat: Infinity,
297
+ delay,
298
+ },
299
+ };
300
+ case 'explode':
301
+ const angle = custom.angle ?? Math.random() * Math.PI * 2;
302
+ const distance = custom.distance ?? 100 + Math.random() * 150;
303
+ return {
304
+ initial: { x: particle.x, y: particle.y, scale: 0, opacity: 1 },
305
+ animate: {
306
+ x: particle.x + Math.cos(angle) * distance,
307
+ y: particle.y + Math.sin(angle) * distance,
308
+ scale: [0, 1.5, 0],
309
+ opacity: [1, 1, 0],
310
+ },
311
+ transition: {
312
+ duration: duration * 0.5,
313
+ ease: [0.25, 0.46, 0.45, 0.94],
314
+ repeat: Infinity,
315
+ repeatDelay: duration * 0.5,
316
+ delay,
317
+ },
318
+ };
319
+ case 'float':
320
+ return {
321
+ initial: { x: particle.x, y: particle.y, opacity: 0, scale: 0.8 },
322
+ animate: {
323
+ x: [particle.x, particle.x + driftAmount, particle.x - driftAmount, particle.x],
324
+ y: [particle.y, particle.y - 20, particle.y + 10, particle.y],
325
+ opacity: [0, 1, 1, 0],
326
+ scale: [0.8, 1, 1.1, 1, 0.8],
327
+ rotate: [0, rotationAmount / 4, -rotationAmount / 4, 0],
328
+ },
329
+ transition: {
330
+ duration,
331
+ ease: 'easeInOut',
332
+ repeat: Infinity,
333
+ delay,
334
+ },
335
+ };
336
+ case 'scatter':
337
+ const scatterAngle = custom.angle ?? Math.random() * Math.PI * 2;
338
+ const scatterDistance = custom.distance ?? 50 + Math.random() * 100;
339
+ return {
340
+ initial: { x: particle.x, y: particle.y, scale: 0, opacity: 0.8 },
341
+ animate: {
342
+ x: particle.x + Math.cos(scatterAngle) * scatterDistance,
343
+ y: particle.y + Math.sin(scatterAngle) * scatterDistance + 50,
344
+ scale: [0, 2, 0],
345
+ opacity: [0.8, 0.6, 0],
346
+ rotate: rotationAmount,
347
+ },
348
+ transition: {
349
+ duration: duration * 0.7,
350
+ ease: 'easeOut',
351
+ repeat: Infinity,
352
+ repeatDelay: duration * 0.3,
353
+ delay,
354
+ },
355
+ };
356
+ default:
357
+ return {
358
+ initial: { opacity: 0 },
359
+ animate: { opacity: 1 },
360
+ transition: { duration: 1 },
361
+ };
362
+ }
363
+ }
364
+ /**
365
+ * Particle component that renders an SVG particle with Framer Motion animations
366
+ */
367
+ const Particle = ({ particle, animationType, physics, }) => {
368
+ const svgString = PARTICLE_SVGS[particle.type];
369
+ const animationProps = getAnimationProps(particle, animationType, physics);
370
+ return React.createElement(motion.div, {
371
+ key: particle.id,
372
+ style: {
373
+ position: 'absolute',
374
+ width: particle.size,
375
+ height: particle.size,
376
+ color: particle.color,
377
+ pointerEvents: 'none',
378
+ },
379
+ initial: animationProps.initial,
380
+ animate: animationProps.animate,
381
+ transition: animationProps.transition,
382
+ dangerouslySetInnerHTML: { __html: svgString },
383
+ });
384
+ };
385
+ /**
386
+ * Generate a unique ID for a particle
387
+ */
388
+ function generateId() {
389
+ return `particle-${Math.random().toString(36).substring(2, 11)}`;
390
+ }
391
+ /**
392
+ * Get a random item from an array
393
+ */
394
+ function randomFromArray(arr) {
395
+ return arr[Math.floor(Math.random() * arr.length)];
396
+ }
397
+ /**
398
+ * Get a random number within a range
399
+ */
400
+ function randomInRange(min, max) {
401
+ return min + Math.random() * (max - min);
402
+ }
403
+ /**
404
+ * Generate particles for a festival effect
405
+ * @param count - Number of particles to generate
406
+ * @param viewport - Viewport dimensions
407
+ * @param particleTypes - Array of particle types to use
408
+ * @param colors - Array of colors to use
409
+ * @param physics - Physics configuration
410
+ * @param animationType - Type of animation
411
+ * @returns Array of ParticleData objects
412
+ */
413
+ function generateParticles(count, viewport, particleTypes, colors, physics, animationType) {
414
+ const particles = [];
415
+ for (let i = 0; i < count; i++) {
416
+ const type = randomFromArray(particleTypes);
417
+ const color = randomFromArray(colors);
418
+ const size = randomInRange(physics.scale[0] * 20, physics.scale[1] * 20);
419
+ // Position calculation based on animation type
420
+ let x;
421
+ let y;
422
+ switch (animationType) {
423
+ case 'fall':
424
+ // Start from random x position at top
425
+ x = randomInRange(0, viewport.width);
426
+ y = randomInRange(-100, -20);
427
+ break;
428
+ case 'rise':
429
+ // Start from random x position at bottom
430
+ x = randomInRange(0, viewport.width);
431
+ y = viewport.height + randomInRange(20, 100);
432
+ break;
433
+ case 'explode':
434
+ // Start from random positions across viewport
435
+ x = randomInRange(viewport.width * 0.2, viewport.width * 0.8);
436
+ y = randomInRange(viewport.height * 0.2, viewport.height * 0.6);
437
+ break;
438
+ case 'float':
439
+ // Random positions across viewport
440
+ x = randomInRange(0, viewport.width);
441
+ y = randomInRange(0, viewport.height);
442
+ break;
443
+ case 'scatter':
444
+ // Start from center-ish positions
445
+ x = randomInRange(viewport.width * 0.3, viewport.width * 0.7);
446
+ y = randomInRange(viewport.height * 0.3, viewport.height * 0.7);
447
+ break;
448
+ default:
449
+ x = randomInRange(0, viewport.width);
450
+ y = randomInRange(0, viewport.height);
451
+ }
452
+ // Calculate delay and duration based on physics speed
453
+ const baseDelay = i * (1 / count) * 3; // Stagger particles
454
+ const delay = baseDelay + randomInRange(0, 0.5);
455
+ const baseDuration = 10 / physics.speed;
456
+ const duration = baseDuration + randomInRange(-1, 1);
457
+ // Custom properties for animation variations
458
+ const custom = {
459
+ drift: physics.drift * (Math.random() - 0.5) * 2,
460
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
461
+ angle: Math.random() * Math.PI * 2,
462
+ distance: 50 + Math.random() * 150,
463
+ };
464
+ particles.push({
465
+ id: generateId(),
466
+ x,
467
+ y,
468
+ size,
469
+ color,
470
+ type,
471
+ delay,
472
+ duration,
473
+ custom,
474
+ });
475
+ }
476
+ return particles;
477
+ }
478
+ /**
479
+ * Calculate the actual particle count based on base count and intensity
480
+ * @param baseCount - The base particle count from festival config
481
+ * @param intensity - The intensity level
482
+ * @returns The calculated particle count, capped at maxParticles
483
+ */
484
+ function calculateParticleCount(baseCount, intensity = 'medium') {
485
+ const settings = INTENSITY_CONFIG[intensity];
486
+ const calculated = Math.floor(baseCount * settings.particleMultiplier);
487
+ return Math.min(calculated, settings.maxParticles);
488
+ }
489
+
490
+ /**
491
+ * Check if a festival type is valid
492
+ */
493
+ function isValidFestival(festival) {
494
+ return FESTIVAL_TYPES.includes(festival);
495
+ }
496
+ /**
497
+ * FestiveEffects - Main component for rendering festival-themed visual effects
498
+ *
499
+ * Renders a full-screen overlay with animated particles for various festivals.
500
+ * Uses Framer Motion for smooth, GPU-accelerated animations.
501
+ *
502
+ * Features:
503
+ * - Automatic cleanup of timeouts and event listeners on unmount
504
+ * - Pauses animations when browser tab is not visible (performance optimization)
505
+ * - Respects user's reduced motion preferences
506
+ *
507
+ * @example
508
+ * ```tsx
509
+ * <FestiveEffects festival="christmas" intensity="medium" />
510
+ * ```
511
+ */
512
+ const FestiveEffects = ({ festival, intensity = 'medium', duration, zIndex = 9999, respectReducedMotion = true, colors, }) => {
513
+ const [particles, setParticles] = useState([]);
514
+ const [isActive, setIsActive] = useState(true);
515
+ const [viewport, setViewport] = useState({ width: 0, height: 0 });
516
+ const prefersReducedMotion = useReducedMotion();
517
+ const isDocumentVisible = useDocumentVisibility();
518
+ // Refs for cleanup tracking
519
+ const durationTimeoutRef = useRef(null);
520
+ const animationFrameRef = useRef(null);
521
+ const isMountedRef = useRef(true);
522
+ // Cleanup function to cancel all pending operations
523
+ const cleanup = useCallback(() => {
524
+ // Clear duration timeout
525
+ if (durationTimeoutRef.current) {
526
+ clearTimeout(durationTimeoutRef.current);
527
+ durationTimeoutRef.current = null;
528
+ }
529
+ // Cancel any pending animation frames
530
+ if (animationFrameRef.current) {
531
+ cancelAnimationFrame(animationFrameRef.current);
532
+ animationFrameRef.current = null;
533
+ }
534
+ }, []);
535
+ // Track mounted state for async operations
536
+ useEffect(() => {
537
+ isMountedRef.current = true;
538
+ return () => {
539
+ isMountedRef.current = false;
540
+ cleanup();
541
+ };
542
+ }, [cleanup]);
543
+ // Update viewport dimensions with cleanup
544
+ useEffect(() => {
545
+ if (typeof window === 'undefined')
546
+ return;
547
+ const updateViewport = () => {
548
+ if (isMountedRef.current) {
549
+ setViewport({
550
+ width: window.innerWidth,
551
+ height: window.innerHeight,
552
+ });
553
+ }
554
+ };
555
+ updateViewport();
556
+ window.addEventListener('resize', updateViewport);
557
+ return () => {
558
+ window.removeEventListener('resize', updateViewport);
559
+ };
560
+ }, []);
561
+ // Handle duration timeout with proper cleanup
562
+ useEffect(() => {
563
+ if (duration && duration > 0) {
564
+ durationTimeoutRef.current = setTimeout(() => {
565
+ if (isMountedRef.current) {
566
+ setIsActive(false);
567
+ }
568
+ }, duration);
569
+ }
570
+ return () => {
571
+ if (durationTimeoutRef.current) {
572
+ clearTimeout(durationTimeoutRef.current);
573
+ durationTimeoutRef.current = null;
574
+ }
575
+ };
576
+ }, [duration]);
577
+ // Generate particles when viewport is ready and effect is active
578
+ useEffect(() => {
579
+ if (!isActive || viewport.width === 0 || viewport.height === 0) {
580
+ return;
581
+ }
582
+ if (!isValidFestival(festival)) {
583
+ return;
584
+ }
585
+ const config = FESTIVAL_CONFIGS[festival];
586
+ const effectColors = colors && colors.length > 0 ? colors : config.colors;
587
+ const particleCount = getParticleCount(config.baseParticleCount, intensity);
588
+ const newParticles = generateParticles(particleCount, viewport, config.particleTypes, effectColors, config.physics, config.animationType);
589
+ if (isMountedRef.current) {
590
+ setParticles(newParticles);
591
+ }
592
+ }, [festival, intensity, colors, viewport, isActive]);
593
+ // Validate festival prop
594
+ if (!isValidFestival(festival)) {
595
+ console.warn(`[FestiveEffects] Invalid festival prop: "${festival}". ` +
596
+ `Valid options are: ${FESTIVAL_TYPES.join(', ')}`);
597
+ return null;
598
+ }
599
+ // Respect reduced motion preference
600
+ if (respectReducedMotion && prefersReducedMotion) {
601
+ return null;
602
+ }
603
+ // Don't render if effect is not active
604
+ if (!isActive) {
605
+ return null;
606
+ }
607
+ // Don't render if viewport is not ready
608
+ if (viewport.width === 0 || viewport.height === 0) {
609
+ return null;
610
+ }
611
+ // Pause animations when document is not visible (tab is in background)
612
+ // This improves performance by not rendering animations that can't be seen
613
+ const shouldRenderParticles = isDocumentVisible;
614
+ const config = FESTIVAL_CONFIGS[festival];
615
+ return (jsx("div", { "data-testid": "festive-effects-overlay", style: {
616
+ position: 'fixed',
617
+ top: 0,
618
+ left: 0,
619
+ width: '100%',
620
+ height: '100%',
621
+ pointerEvents: 'none',
622
+ zIndex,
623
+ overflow: 'hidden',
624
+ }, children: jsx(AnimatePresence, { children: shouldRenderParticles && particles.map((particle) => (jsx(Particle, { particle: particle, animationType: config.animationType, physics: config.physics }, particle.id))) }) }));
625
+ };
626
+
627
+ // Animation System for Festive Effects
628
+ // Contains Framer Motion variants for each animation type
629
+ /**
630
+ * Fall animation variants - used for snow, leaves, shamrocks
631
+ * Particles fall from top to bottom with horizontal drift and rotation
632
+ */
633
+ const fallVariants = {
634
+ initial: {
635
+ y: -50,
636
+ opacity: 0,
637
+ rotate: 0,
638
+ },
639
+ animate: (custom) => ({
640
+ y: custom.viewportHeight + 50,
641
+ x: custom.drift,
642
+ opacity: [0, 1, 1, 0],
643
+ rotate: custom.rotation,
644
+ }),
645
+ };
646
+ /**
647
+ * Get transition for fall animation
648
+ */
649
+ function getFallTransition(custom) {
650
+ return {
651
+ duration: custom.duration,
652
+ ease: 'linear',
653
+ repeat: Infinity,
654
+ delay: custom.delay,
655
+ };
656
+ }
657
+ /**
658
+ * Rise animation variants - used for hearts, lanterns
659
+ * Particles rise from bottom to top with gentle sway
660
+ */
661
+ const riseVariants = {
662
+ initial: (custom) => ({
663
+ y: custom.viewportHeight + 50,
664
+ opacity: 0,
665
+ scale: 0,
666
+ }),
667
+ animate: (custom) => ({
668
+ y: -50,
669
+ x: [0, custom.drift / 2, -custom.drift / 2, 0],
670
+ opacity: [0, 1, 1, 0],
671
+ scale: [0, 1, 1.2, 1, 0],
672
+ }),
673
+ };
674
+ /**
675
+ * Get transition for rise animation
676
+ */
677
+ function getRiseTransition(custom) {
678
+ return {
679
+ duration: custom.duration,
680
+ ease: 'easeOut',
681
+ repeat: Infinity,
682
+ delay: custom.delay,
683
+ };
684
+ }
685
+ /**
686
+ * Explode animation variants - used for fireworks, confetti
687
+ * Particles burst outward from center point
688
+ */
689
+ const explodeVariants = {
690
+ initial: {
691
+ scale: 0,
692
+ opacity: 1,
693
+ },
694
+ animate: (custom) => ({
695
+ x: Math.cos(custom.angle) * custom.distance,
696
+ y: Math.sin(custom.angle) * custom.distance,
697
+ scale: [0, 1.5, 0],
698
+ opacity: [1, 1, 0],
699
+ }),
700
+ };
701
+ /**
702
+ * Get transition for explode animation
703
+ */
704
+ function getExplodeTransition(custom) {
705
+ return {
706
+ duration: custom.duration * 0.5,
707
+ ease: [0.25, 0.46, 0.45, 0.94],
708
+ repeat: Infinity,
709
+ repeatDelay: custom.duration * 0.5,
710
+ delay: custom.delay,
711
+ };
712
+ }
713
+ /**
714
+ * Float animation variants - used for bats, diyas, moons
715
+ * Particles float with gentle bobbing motion
716
+ */
717
+ const floatVariants = {
718
+ initial: {
719
+ opacity: 0,
720
+ scale: 0.8,
721
+ },
722
+ animate: (custom) => ({
723
+ x: [0, custom.drift, -custom.drift, 0],
724
+ y: [0, -20, 10, 0],
725
+ opacity: [0, 1, 1, 0],
726
+ scale: [0.8, 1, 1.1, 1, 0.8],
727
+ rotate: [0, custom.rotation / 4, -custom.rotation / 4, 0],
728
+ }),
729
+ };
730
+ /**
731
+ * Get transition for float animation
732
+ */
733
+ function getFloatTransition(custom) {
734
+ return {
735
+ duration: custom.duration,
736
+ ease: 'easeInOut',
737
+ repeat: Infinity,
738
+ delay: custom.delay,
739
+ };
740
+ }
741
+ /**
742
+ * Scatter animation variants - used for holi colors
743
+ * Particles scatter outward with fading effect
744
+ */
745
+ const scatterVariants = {
746
+ initial: {
747
+ scale: 0,
748
+ opacity: 0.8,
749
+ },
750
+ animate: (custom) => ({
751
+ x: Math.cos(custom.angle) * custom.distance,
752
+ y: Math.sin(custom.angle) * custom.distance + 50,
753
+ scale: [0, 2, 0],
754
+ opacity: [0.8, 0.6, 0],
755
+ rotate: custom.rotation,
756
+ }),
757
+ };
758
+ /**
759
+ * Get transition for scatter animation
760
+ */
761
+ function getScatterTransition(custom) {
762
+ return {
763
+ duration: custom.duration * 0.7,
764
+ ease: 'easeOut',
765
+ repeat: Infinity,
766
+ repeatDelay: custom.duration * 0.3,
767
+ delay: custom.delay,
768
+ };
769
+ }
770
+ /**
771
+ * Map of animation types to their variants
772
+ */
773
+ const ANIMATION_VARIANTS = {
774
+ fall: fallVariants,
775
+ rise: riseVariants,
776
+ explode: explodeVariants,
777
+ float: floatVariants,
778
+ scatter: scatterVariants,
779
+ };
780
+ /**
781
+ * Get the appropriate transition function for an animation type
782
+ */
783
+ function getTransitionForType(animationType, custom) {
784
+ switch (animationType) {
785
+ case 'fall':
786
+ return getFallTransition(custom);
787
+ case 'rise':
788
+ return getRiseTransition(custom);
789
+ case 'explode':
790
+ return getExplodeTransition(custom);
791
+ case 'float':
792
+ return getFloatTransition(custom);
793
+ case 'scatter':
794
+ return getScatterTransition(custom);
795
+ default:
796
+ return { duration: 1 };
797
+ }
798
+ }
799
+ /**
800
+ * Create custom animation data from particle properties
801
+ */
802
+ function createAnimationCustomData(x, y, physics, delay, duration, viewportHeight) {
803
+ return {
804
+ drift: physics.drift * (Math.random() - 0.5) * 2,
805
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
806
+ delay,
807
+ duration,
808
+ angle: Math.random() * Math.PI * 2,
809
+ distance: 50 + Math.random() * 150,
810
+ x,
811
+ y,
812
+ viewportHeight,
813
+ };
814
+ }
815
+ /**
816
+ * Get animation props for a particle based on animation type
817
+ * Returns initial, animate, and transition properties for Framer Motion
818
+ */
819
+ function getAnimationPropsForType(animationType, custom) {
820
+ const transition = getTransitionForType(animationType, custom);
821
+ // Get initial and animate values based on animation type
822
+ // We compute these directly rather than using variant functions
823
+ // to avoid TypeScript issues with Framer Motion's TargetResolver signature
824
+ let initial;
825
+ let animate;
826
+ switch (animationType) {
827
+ case 'fall':
828
+ initial = { y: -50, opacity: 0, rotate: 0 };
829
+ animate = {
830
+ y: custom.viewportHeight + 50,
831
+ x: custom.drift,
832
+ opacity: [0, 1, 1, 0],
833
+ rotate: custom.rotation,
834
+ };
835
+ break;
836
+ case 'rise':
837
+ initial = { y: custom.viewportHeight + 50, opacity: 0, scale: 0 };
838
+ animate = {
839
+ y: -50,
840
+ x: [0, custom.drift / 2, -custom.drift / 2, 0],
841
+ opacity: [0, 1, 1, 0],
842
+ scale: [0, 1, 1.2, 1, 0],
843
+ };
844
+ break;
845
+ case 'explode':
846
+ initial = { scale: 0, opacity: 1 };
847
+ animate = {
848
+ x: Math.cos(custom.angle) * custom.distance,
849
+ y: Math.sin(custom.angle) * custom.distance,
850
+ scale: [0, 1.5, 0],
851
+ opacity: [1, 1, 0],
852
+ };
853
+ break;
854
+ case 'float':
855
+ initial = { opacity: 0, scale: 0.8 };
856
+ animate = {
857
+ x: [0, custom.drift, -custom.drift, 0],
858
+ y: [0, -20, 10, 0],
859
+ opacity: [0, 1, 1, 0],
860
+ scale: [0.8, 1, 1.1, 1, 0.8],
861
+ rotate: [0, custom.rotation / 4, -custom.rotation / 4, 0],
862
+ };
863
+ break;
864
+ case 'scatter':
865
+ initial = { scale: 0, opacity: 0.8 };
866
+ animate = {
867
+ x: Math.cos(custom.angle) * custom.distance,
868
+ y: Math.sin(custom.angle) * custom.distance + 50,
869
+ scale: [0, 2, 0],
870
+ opacity: [0.8, 0.6, 0],
871
+ rotate: custom.rotation,
872
+ };
873
+ break;
874
+ default:
875
+ initial = { opacity: 0 };
876
+ animate = { opacity: 1 };
877
+ }
878
+ return {
879
+ initial,
880
+ animate,
881
+ transition,
882
+ };
883
+ }
884
+
885
+ // Festival Registry for Festive Effects
886
+ // Manages registration and retrieval of festival effects
887
+ /**
888
+ * Create a festival effect from a festival type
889
+ * @param festivalType - The type of festival
890
+ * @returns A FestivalEffect instance
891
+ */
892
+ function createFestivalEffect(festivalType) {
893
+ const config = FESTIVAL_CONFIGS[festivalType];
894
+ return {
895
+ name: festivalType,
896
+ generateParticles(count, viewport) {
897
+ return generateParticles(count, viewport, config.particleTypes, config.colors, config.physics, config.animationType);
898
+ },
899
+ getConfig() {
900
+ return config;
901
+ },
902
+ renderParticle(particle) {
903
+ const props = {
904
+ particle,
905
+ animationType: config.animationType,
906
+ physics: config.physics,
907
+ };
908
+ return React.createElement(Particle, { key: particle.id, ...props });
909
+ },
910
+ };
911
+ }
912
+ /**
913
+ * Registry for managing festival effects
914
+ * Provides methods to register, retrieve, and list festival effects
915
+ */
916
+ class FestivalRegistry {
917
+ constructor() {
918
+ this.effects = new Map();
919
+ }
920
+ /**
921
+ * Register a festival effect
922
+ * @param effect - The festival effect to register
923
+ */
924
+ register(effect) {
925
+ this.effects.set(effect.name, effect);
926
+ }
927
+ /**
928
+ * Get a festival effect by type
929
+ * @param festival - The festival type to retrieve
930
+ * @returns The festival effect or undefined if not found
931
+ */
932
+ get(festival) {
933
+ return this.effects.get(festival);
934
+ }
935
+ /**
936
+ * Check if a festival effect is registered
937
+ * @param festival - The festival type to check
938
+ * @returns True if the effect is registered
939
+ */
940
+ has(festival) {
941
+ return this.effects.has(festival);
942
+ }
943
+ /**
944
+ * List all registered festival types
945
+ * @returns Array of registered festival types
946
+ */
947
+ list() {
948
+ return Array.from(this.effects.keys());
949
+ }
950
+ /**
951
+ * Get the number of registered effects
952
+ * @returns The count of registered effects
953
+ */
954
+ get size() {
955
+ return this.effects.size;
956
+ }
957
+ /**
958
+ * Clear all registered effects
959
+ */
960
+ clear() {
961
+ this.effects.clear();
962
+ }
963
+ }
964
+ /**
965
+ * Default registry instance with all 12 festival effects pre-registered
966
+ */
967
+ const defaultRegistry = new FestivalRegistry();
968
+ // Register all 12 festival effects
969
+ const ALL_FESTIVALS = [
970
+ 'christmas',
971
+ 'newyear',
972
+ 'valentine',
973
+ 'easter',
974
+ 'halloween',
975
+ 'thanksgiving',
976
+ 'diwali',
977
+ 'chinesenewyear',
978
+ 'holi',
979
+ 'eid',
980
+ 'stpatricks',
981
+ 'independence',
982
+ ];
983
+ ALL_FESTIVALS.forEach((festivalType) => {
984
+ defaultRegistry.register(createFestivalEffect(festivalType));
985
+ });
986
+
987
+ /**
988
+ * Christmas effect configuration
989
+ */
990
+ const CHRISTMAS_CONFIG = {
991
+ baseParticleCount: 50,
992
+ colors: ['#FFFFFF', '#E8F4FF', '#B8D4E8', '#87CEEB'],
993
+ particleTypes: ['snowflake'],
994
+ animationType: 'fall',
995
+ physics: {
996
+ speed: 3,
997
+ drift: 50,
998
+ rotation: 360,
999
+ scale: [0.5, 1.5],
1000
+ },
1001
+ };
1002
+ /**
1003
+ * Individual snowflake particle with falling animation
1004
+ */
1005
+ const ChristmasParticle = ({ particle, viewportHeight }) => {
1006
+ const { drift, rotation } = particle.custom;
1007
+ return (jsx(motion.div, { style: {
1008
+ position: 'absolute',
1009
+ left: particle.x,
1010
+ width: particle.size,
1011
+ height: particle.size,
1012
+ color: particle.color,
1013
+ pointerEvents: 'none',
1014
+ }, initial: { y: -50, opacity: 0, rotate: 0 }, animate: {
1015
+ y: viewportHeight + 50,
1016
+ x: drift,
1017
+ opacity: [0, 1, 1, 0],
1018
+ rotate: rotation,
1019
+ }, transition: {
1020
+ duration: particle.duration,
1021
+ ease: 'linear',
1022
+ repeat: Infinity,
1023
+ delay: particle.delay,
1024
+ }, dangerouslySetInnerHTML: { __html: PARTICLE_SVGS.snowflake } }));
1025
+ };
1026
+ /**
1027
+ * Generate snowflake particles for Christmas effect
1028
+ */
1029
+ function generateChristmasParticles(count, viewport, colors) {
1030
+ const effectColors = colors && colors.length > 0 ? colors : CHRISTMAS_CONFIG.colors;
1031
+ const physics = CHRISTMAS_CONFIG.physics;
1032
+ const particles = [];
1033
+ for (let i = 0; i < count; i++) {
1034
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1035
+ const baseDelay = i * (1 / count) * 3;
1036
+ particles.push({
1037
+ id: `christmas-${i}-${Math.random().toString(36).substring(2, 9)}`,
1038
+ x: Math.random() * viewport.width,
1039
+ y: -50 - Math.random() * 80,
1040
+ size,
1041
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1042
+ type: 'snowflake',
1043
+ delay: baseDelay + Math.random() * 0.5,
1044
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1045
+ custom: {
1046
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1047
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1048
+ },
1049
+ });
1050
+ }
1051
+ return particles;
1052
+ }
1053
+ /**
1054
+ * Christmas Effect Component
1055
+ * Renders falling snowflakes with drift and rotation
1056
+ * White/blue color palette
1057
+ */
1058
+ const ChristmasEffect = ({ particles, viewport }) => {
1059
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(ChristmasParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
1060
+ };
1061
+
1062
+ /**
1063
+ * New Year effect configuration
1064
+ */
1065
+ const NEWYEAR_CONFIG = {
1066
+ baseParticleCount: 40,
1067
+ colors: ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'],
1068
+ particleTypes: ['firework', 'confetti', 'spark'],
1069
+ animationType: 'explode',
1070
+ physics: {
1071
+ speed: 5,
1072
+ drift: 100,
1073
+ rotation: 720,
1074
+ scale: [0.3, 1.2],
1075
+ },
1076
+ };
1077
+ /**
1078
+ * Individual particle with explode animation for fireworks/confetti/sparks
1079
+ */
1080
+ const NewYearParticle = ({ particle }) => {
1081
+ const { angle, distance } = particle.custom;
1082
+ const svgString = PARTICLE_SVGS[particle.type];
1083
+ return (jsx(motion.div, { style: {
1084
+ position: 'absolute',
1085
+ left: particle.x,
1086
+ top: particle.y,
1087
+ width: particle.size,
1088
+ height: particle.size,
1089
+ color: particle.color,
1090
+ pointerEvents: 'none',
1091
+ }, initial: { scale: 0, opacity: 1 }, animate: {
1092
+ x: Math.cos(angle) * distance,
1093
+ y: Math.sin(angle) * distance,
1094
+ scale: [0, 1.5, 0],
1095
+ opacity: [1, 1, 0],
1096
+ }, transition: {
1097
+ duration: particle.duration * 0.5,
1098
+ ease: [0.25, 0.46, 0.45, 0.94],
1099
+ repeat: Infinity,
1100
+ repeatDelay: particle.duration * 0.5,
1101
+ delay: particle.delay,
1102
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1103
+ };
1104
+ /**
1105
+ * Generate particles for New Year effect
1106
+ */
1107
+ function generateNewYearParticles(count, viewport, colors) {
1108
+ const effectColors = colors && colors.length > 0 ? colors : NEWYEAR_CONFIG.colors;
1109
+ const physics = NEWYEAR_CONFIG.physics;
1110
+ const particles = [];
1111
+ const particleTypes = NEWYEAR_CONFIG.particleTypes;
1112
+ for (let i = 0; i < count; i++) {
1113
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1114
+ const baseDelay = i * (1 / count) * 3;
1115
+ const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
1116
+ particles.push({
1117
+ id: `newyear-${i}-${Math.random().toString(36).substring(2, 9)}`,
1118
+ x: viewport.width * 0.2 + Math.random() * viewport.width * 0.6,
1119
+ y: viewport.height * 0.2 + Math.random() * viewport.height * 0.4,
1120
+ size,
1121
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1122
+ type,
1123
+ delay: baseDelay + Math.random() * 0.5,
1124
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1125
+ custom: {
1126
+ angle: Math.random() * Math.PI * 2,
1127
+ distance: 100 + Math.random() * 150,
1128
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1129
+ },
1130
+ });
1131
+ }
1132
+ return particles;
1133
+ }
1134
+ /**
1135
+ * New Year Effect Component
1136
+ * Renders exploding fireworks with sparks and falling confetti
1137
+ */
1138
+ const NewYearEffect = ({ particles }) => {
1139
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(NewYearParticle, { particle: particle }, particle.id))) }));
1140
+ };
1141
+
1142
+ /**
1143
+ * Valentine effect configuration
1144
+ */
1145
+ const VALENTINE_CONFIG = {
1146
+ baseParticleCount: 35,
1147
+ colors: ['#FF6B6B', '#EE5A5A', '#FF8E8E', '#FFB6C1', '#FF69B4'],
1148
+ particleTypes: ['heart'],
1149
+ animationType: 'rise',
1150
+ physics: {
1151
+ speed: 2,
1152
+ drift: 30,
1153
+ rotation: 15,
1154
+ scale: [0.6, 1.4],
1155
+ },
1156
+ };
1157
+ /**
1158
+ * Individual heart particle with rising animation and gentle sway
1159
+ */
1160
+ const ValentineParticle = ({ particle, viewportHeight }) => {
1161
+ const { drift } = particle.custom;
1162
+ return (jsx(motion.div, { style: {
1163
+ position: 'absolute',
1164
+ left: particle.x,
1165
+ width: particle.size,
1166
+ height: particle.size,
1167
+ color: particle.color,
1168
+ pointerEvents: 'none',
1169
+ }, initial: { y: viewportHeight + 50, opacity: 0, scale: 0 }, animate: {
1170
+ y: -50,
1171
+ x: [0, drift / 2, -drift / 2, 0],
1172
+ opacity: [0, 1, 1, 0],
1173
+ scale: [0, 1, 1.2, 1, 0],
1174
+ }, transition: {
1175
+ duration: particle.duration,
1176
+ ease: 'easeOut',
1177
+ repeat: Infinity,
1178
+ delay: particle.delay,
1179
+ }, dangerouslySetInnerHTML: { __html: PARTICLE_SVGS.heart } }));
1180
+ };
1181
+ /**
1182
+ * Generate heart particles for Valentine effect
1183
+ */
1184
+ function generateValentineParticles(count, viewport, colors) {
1185
+ const effectColors = colors && colors.length > 0 ? colors : VALENTINE_CONFIG.colors;
1186
+ const physics = VALENTINE_CONFIG.physics;
1187
+ const particles = [];
1188
+ for (let i = 0; i < count; i++) {
1189
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1190
+ const baseDelay = i * (1 / count) * 3;
1191
+ particles.push({
1192
+ id: `valentine-${i}-${Math.random().toString(36).substring(2, 9)}`,
1193
+ x: Math.random() * viewport.width,
1194
+ y: viewport.height + 50 + Math.random() * 80,
1195
+ size,
1196
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1197
+ type: 'heart',
1198
+ delay: baseDelay + Math.random() * 0.5,
1199
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1200
+ custom: {
1201
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1202
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1203
+ },
1204
+ });
1205
+ }
1206
+ return particles;
1207
+ }
1208
+ /**
1209
+ * Valentine Effect Component
1210
+ * Renders rising hearts with gentle sway
1211
+ * Pink/red color palette
1212
+ */
1213
+ const ValentineEffect = ({ particles, viewport }) => {
1214
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(ValentineParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
1215
+ };
1216
+
1217
+ /**
1218
+ * Easter effect configuration
1219
+ */
1220
+ const EASTER_CONFIG = {
1221
+ baseParticleCount: 30,
1222
+ colors: ['#FFB6C1', '#98FB98', '#87CEEB', '#DDA0DD', '#F0E68C'],
1223
+ particleTypes: ['egg', 'flower'],
1224
+ animationType: 'float',
1225
+ physics: {
1226
+ speed: 1.5,
1227
+ drift: 20,
1228
+ rotation: 10,
1229
+ scale: [0.7, 1.3],
1230
+ },
1231
+ };
1232
+ /**
1233
+ * Individual egg/flower particle with floating animation
1234
+ */
1235
+ const EasterParticle = ({ particle }) => {
1236
+ const { drift, rotation } = particle.custom;
1237
+ const svgString = PARTICLE_SVGS[particle.type];
1238
+ return (jsx(motion.div, { style: {
1239
+ position: 'absolute',
1240
+ left: particle.x,
1241
+ top: particle.y,
1242
+ width: particle.size,
1243
+ height: particle.size,
1244
+ color: particle.color,
1245
+ pointerEvents: 'none',
1246
+ }, initial: { opacity: 0, scale: 0.8 }, animate: {
1247
+ x: [0, drift, -drift, 0],
1248
+ y: [0, -20, 10, 0],
1249
+ opacity: [0, 1, 1, 0],
1250
+ scale: [0.8, 1, 1.1, 1, 0.8],
1251
+ rotate: [0, rotation / 4, -rotation / 4, 0],
1252
+ }, transition: {
1253
+ duration: particle.duration,
1254
+ ease: 'easeInOut',
1255
+ repeat: Infinity,
1256
+ delay: particle.delay,
1257
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1258
+ };
1259
+ /**
1260
+ * Generate particles for Easter effect
1261
+ */
1262
+ function generateEasterParticles(count, viewport, colors) {
1263
+ const effectColors = colors && colors.length > 0 ? colors : EASTER_CONFIG.colors;
1264
+ const physics = EASTER_CONFIG.physics;
1265
+ const particles = [];
1266
+ const particleTypes = EASTER_CONFIG.particleTypes;
1267
+ for (let i = 0; i < count; i++) {
1268
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1269
+ const baseDelay = i * (1 / count) * 3;
1270
+ const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
1271
+ particles.push({
1272
+ id: `easter-${i}-${Math.random().toString(36).substring(2, 9)}`,
1273
+ x: Math.random() * viewport.width,
1274
+ y: Math.random() * viewport.height,
1275
+ size,
1276
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1277
+ type,
1278
+ delay: baseDelay + Math.random() * 0.5,
1279
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1280
+ custom: {
1281
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1282
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1283
+ },
1284
+ });
1285
+ }
1286
+ return particles;
1287
+ }
1288
+ /**
1289
+ * Easter Effect Component
1290
+ * Renders floating eggs and spring flowers
1291
+ * Pastel color palette
1292
+ */
1293
+ const EasterEffect = ({ particles }) => {
1294
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(EasterParticle, { particle: particle }, particle.id))) }));
1295
+ };
1296
+
1297
+ /**
1298
+ * Halloween effect configuration
1299
+ */
1300
+ const HALLOWEEN_CONFIG = {
1301
+ baseParticleCount: 35,
1302
+ colors: ['#FF6600', '#8B008B', '#000000', '#FFD700', '#32CD32'],
1303
+ particleTypes: ['bat', 'pumpkin', 'spider'],
1304
+ animationType: 'float',
1305
+ physics: {
1306
+ speed: 2,
1307
+ drift: 60,
1308
+ rotation: 30,
1309
+ scale: [0.5, 1.5],
1310
+ },
1311
+ };
1312
+ /**
1313
+ * Individual bat/pumpkin/spider particle with erratic floating animation
1314
+ */
1315
+ const HalloweenParticle = ({ particle }) => {
1316
+ const { drift, rotation } = particle.custom;
1317
+ const svgString = PARTICLE_SVGS[particle.type];
1318
+ // Bats have more erratic movement
1319
+ const isBat = particle.type === 'bat';
1320
+ const driftMultiplier = isBat ? 1.5 : 1;
1321
+ const rotationMultiplier = isBat ? 2 : 1;
1322
+ return (jsx(motion.div, { style: {
1323
+ position: 'absolute',
1324
+ left: particle.x,
1325
+ top: particle.y,
1326
+ width: particle.size,
1327
+ height: particle.size,
1328
+ color: particle.color,
1329
+ pointerEvents: 'none',
1330
+ }, initial: { opacity: 0, scale: 0.8 }, animate: {
1331
+ x: [0, drift * driftMultiplier, -drift * driftMultiplier, drift * 0.5, 0],
1332
+ y: [0, -30, 15, -20, 0],
1333
+ opacity: [0, 1, 1, 1, 0],
1334
+ scale: [0.8, 1, 1.1, 1, 0.8],
1335
+ rotate: [0, rotation * rotationMultiplier / 4, -rotation * rotationMultiplier / 4, rotation / 8, 0],
1336
+ }, transition: {
1337
+ duration: particle.duration,
1338
+ ease: 'easeInOut',
1339
+ repeat: Infinity,
1340
+ delay: particle.delay,
1341
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1342
+ };
1343
+ /**
1344
+ * Generate particles for Halloween effect
1345
+ */
1346
+ function generateHalloweenParticles(count, viewport, colors) {
1347
+ const effectColors = colors && colors.length > 0 ? colors : HALLOWEEN_CONFIG.colors;
1348
+ const physics = HALLOWEEN_CONFIG.physics;
1349
+ const particles = [];
1350
+ const particleTypes = HALLOWEEN_CONFIG.particleTypes;
1351
+ for (let i = 0; i < count; i++) {
1352
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1353
+ const baseDelay = i * (1 / count) * 3;
1354
+ const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
1355
+ particles.push({
1356
+ id: `halloween-${i}-${Math.random().toString(36).substring(2, 9)}`,
1357
+ x: Math.random() * viewport.width,
1358
+ y: Math.random() * viewport.height,
1359
+ size,
1360
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1361
+ type,
1362
+ delay: baseDelay + Math.random() * 0.5,
1363
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1364
+ custom: {
1365
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1366
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1367
+ },
1368
+ });
1369
+ }
1370
+ return particles;
1371
+ }
1372
+ /**
1373
+ * Halloween Effect Component
1374
+ * Renders flying bats with erratic movement, floating pumpkins and spiders
1375
+ * Orange/purple/black color palette
1376
+ */
1377
+ const HalloweenEffect = ({ particles }) => {
1378
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(HalloweenParticle, { particle: particle }, particle.id))) }));
1379
+ };
1380
+
1381
+ /**
1382
+ * Thanksgiving effect configuration
1383
+ */
1384
+ const THANKSGIVING_CONFIG = {
1385
+ baseParticleCount: 40,
1386
+ colors: ['#D2691E', '#FF8C00', '#B8860B', '#CD853F', '#8B4513'],
1387
+ particleTypes: ['leaf'],
1388
+ animationType: 'fall',
1389
+ physics: {
1390
+ speed: 2,
1391
+ drift: 80,
1392
+ rotation: 540,
1393
+ scale: [0.6, 1.4],
1394
+ },
1395
+ };
1396
+ /**
1397
+ * Individual leaf particle with tumbling fall animation
1398
+ */
1399
+ const ThanksgivingParticle = ({ particle, viewportHeight }) => {
1400
+ const { drift, rotation } = particle.custom;
1401
+ return (jsx(motion.div, { style: {
1402
+ position: 'absolute',
1403
+ left: particle.x,
1404
+ width: particle.size,
1405
+ height: particle.size,
1406
+ color: particle.color,
1407
+ pointerEvents: 'none',
1408
+ }, initial: { y: -50, opacity: 0, rotate: 0 }, animate: {
1409
+ y: viewportHeight + 50,
1410
+ x: [0, drift * 0.3, -drift * 0.2, drift * 0.4, drift],
1411
+ opacity: [0, 1, 1, 1, 0],
1412
+ rotate: rotation,
1413
+ }, transition: {
1414
+ duration: particle.duration,
1415
+ ease: 'linear',
1416
+ repeat: Infinity,
1417
+ delay: particle.delay,
1418
+ }, dangerouslySetInnerHTML: { __html: PARTICLE_SVGS.leaf } }));
1419
+ };
1420
+ /**
1421
+ * Generate leaf particles for Thanksgiving effect
1422
+ */
1423
+ function generateThanksgivingParticles(count, viewport, colors) {
1424
+ const effectColors = colors && colors.length > 0 ? colors : THANKSGIVING_CONFIG.colors;
1425
+ const physics = THANKSGIVING_CONFIG.physics;
1426
+ const particles = [];
1427
+ for (let i = 0; i < count; i++) {
1428
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1429
+ const baseDelay = i * (1 / count) * 3;
1430
+ particles.push({
1431
+ id: `thanksgiving-${i}-${Math.random().toString(36).substring(2, 9)}`,
1432
+ x: Math.random() * viewport.width,
1433
+ y: -50 - Math.random() * 80,
1434
+ size,
1435
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1436
+ type: 'leaf',
1437
+ delay: baseDelay + Math.random() * 0.5,
1438
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1439
+ custom: {
1440
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1441
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1442
+ },
1443
+ });
1444
+ }
1445
+ return particles;
1446
+ }
1447
+ /**
1448
+ * Thanksgiving Effect Component
1449
+ * Renders falling autumn leaves with tumbling rotation
1450
+ * Orange/brown color palette
1451
+ */
1452
+ const ThanksgivingEffect = ({ particles, viewport }) => {
1453
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(ThanksgivingParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
1454
+ };
1455
+
1456
+ /**
1457
+ * Diwali effect configuration
1458
+ */
1459
+ const DIWALI_CONFIG = {
1460
+ baseParticleCount: 45,
1461
+ colors: ['#FFD700', '#FF8C00', '#FF4500', '#FFA500', '#FFFF00'],
1462
+ particleTypes: ['diya', 'spark', 'rangoli'],
1463
+ animationType: 'float',
1464
+ physics: {
1465
+ speed: 1,
1466
+ drift: 15,
1467
+ rotation: 5,
1468
+ scale: [0.5, 1.2],
1469
+ },
1470
+ };
1471
+ /**
1472
+ * Individual diya/spark/rangoli particle with glowing float animation
1473
+ */
1474
+ const DiwaliParticle = ({ particle }) => {
1475
+ const { drift, rotation } = particle.custom;
1476
+ const svgString = PARTICLE_SVGS[particle.type];
1477
+ // Diyas have flickering effect, sparks have more movement
1478
+ const isDiya = particle.type === 'diya';
1479
+ const isSpark = particle.type === 'spark';
1480
+ return (jsx(motion.div, { style: {
1481
+ position: 'absolute',
1482
+ left: particle.x,
1483
+ top: particle.y,
1484
+ width: particle.size,
1485
+ height: particle.size,
1486
+ color: particle.color,
1487
+ pointerEvents: 'none',
1488
+ filter: isDiya ? 'drop-shadow(0 0 8px #FFD700)' : isSpark ? 'drop-shadow(0 0 4px #FFA500)' : undefined,
1489
+ }, initial: { opacity: 0, scale: 0.8 }, animate: {
1490
+ x: [0, drift, -drift, 0],
1491
+ y: [0, -10, 5, 0],
1492
+ opacity: isDiya ? [0, 1, 0.8, 1, 0] : [0, 1, 1, 0],
1493
+ scale: isDiya ? [0.8, 1, 0.95, 1.05, 0.8] : [0.8, 1, 1.1, 1, 0.8],
1494
+ rotate: [0, rotation / 4, -rotation / 4, 0],
1495
+ }, transition: {
1496
+ duration: particle.duration,
1497
+ ease: 'easeInOut',
1498
+ repeat: Infinity,
1499
+ delay: particle.delay,
1500
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1501
+ };
1502
+ /**
1503
+ * Generate particles for Diwali effect
1504
+ */
1505
+ function generateDiwaliParticles(count, viewport, colors) {
1506
+ const effectColors = colors && colors.length > 0 ? colors : DIWALI_CONFIG.colors;
1507
+ const physics = DIWALI_CONFIG.physics;
1508
+ const particles = [];
1509
+ const particleTypes = DIWALI_CONFIG.particleTypes;
1510
+ for (let i = 0; i < count; i++) {
1511
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1512
+ const baseDelay = i * (1 / count) * 3;
1513
+ const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
1514
+ particles.push({
1515
+ id: `diwali-${i}-${Math.random().toString(36).substring(2, 9)}`,
1516
+ x: Math.random() * viewport.width,
1517
+ y: Math.random() * viewport.height,
1518
+ size,
1519
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1520
+ type,
1521
+ delay: baseDelay + Math.random() * 0.5,
1522
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1523
+ custom: {
1524
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1525
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1526
+ },
1527
+ });
1528
+ }
1529
+ return particles;
1530
+ }
1531
+ /**
1532
+ * Diwali Effect Component
1533
+ * Renders glowing diyas with flickering, golden sparkles and rangoli patterns
1534
+ * Golden/orange color palette
1535
+ */
1536
+ const DiwaliEffect = ({ particles }) => {
1537
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(DiwaliParticle, { particle: particle }, particle.id))) }));
1538
+ };
1539
+
1540
+ /**
1541
+ * Chinese New Year effect configuration
1542
+ */
1543
+ const CHINESENEWYEAR_CONFIG = {
1544
+ baseParticleCount: 40,
1545
+ colors: ['#FF0000', '#FFD700', '#FF4500', '#DC143C'],
1546
+ particleTypes: ['lantern', 'firework', 'dragon'],
1547
+ animationType: 'rise',
1548
+ physics: {
1549
+ speed: 1.5,
1550
+ drift: 25,
1551
+ rotation: 10,
1552
+ scale: [0.6, 1.3],
1553
+ },
1554
+ };
1555
+ /**
1556
+ * Individual lantern/firework/dragon particle with rising animation
1557
+ */
1558
+ const ChineseNewYearParticle = ({ particle, viewportHeight }) => {
1559
+ const { drift, angle, distance } = particle.custom;
1560
+ const svgString = PARTICLE_SVGS[particle.type];
1561
+ // Fireworks explode, lanterns and dragons rise
1562
+ const isFirework = particle.type === 'firework';
1563
+ const isLantern = particle.type === 'lantern';
1564
+ if (isFirework) {
1565
+ return (jsx(motion.div, { style: {
1566
+ position: 'absolute',
1567
+ left: particle.x,
1568
+ top: particle.y,
1569
+ width: particle.size,
1570
+ height: particle.size,
1571
+ color: particle.color,
1572
+ pointerEvents: 'none',
1573
+ }, initial: { scale: 0, opacity: 1 }, animate: {
1574
+ x: Math.cos(angle) * distance,
1575
+ y: Math.sin(angle) * distance,
1576
+ scale: [0, 1.5, 0],
1577
+ opacity: [1, 1, 0],
1578
+ }, transition: {
1579
+ duration: particle.duration * 0.5,
1580
+ ease: [0.25, 0.46, 0.45, 0.94],
1581
+ repeat: Infinity,
1582
+ repeatDelay: particle.duration * 0.5,
1583
+ delay: particle.delay,
1584
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1585
+ }
1586
+ return (jsx(motion.div, { style: {
1587
+ position: 'absolute',
1588
+ left: particle.x,
1589
+ width: particle.size,
1590
+ height: particle.size,
1591
+ color: particle.color,
1592
+ pointerEvents: 'none',
1593
+ filter: isLantern ? 'drop-shadow(0 0 6px #FF0000)' : undefined,
1594
+ }, initial: { y: viewportHeight + 50, opacity: 0, scale: 0 }, animate: {
1595
+ y: -50,
1596
+ x: [0, drift / 2, -drift / 2, 0],
1597
+ opacity: [0, 1, 1, 0],
1598
+ scale: [0, 1, 1.1, 1, 0],
1599
+ }, transition: {
1600
+ duration: particle.duration,
1601
+ ease: 'easeOut',
1602
+ repeat: Infinity,
1603
+ delay: particle.delay,
1604
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1605
+ };
1606
+ /**
1607
+ * Generate particles for Chinese New Year effect
1608
+ */
1609
+ function generateChineseNewYearParticles(count, viewport, colors) {
1610
+ const effectColors = colors && colors.length > 0 ? colors : CHINESENEWYEAR_CONFIG.colors;
1611
+ const physics = CHINESENEWYEAR_CONFIG.physics;
1612
+ const particles = [];
1613
+ const particleTypes = CHINESENEWYEAR_CONFIG.particleTypes;
1614
+ for (let i = 0; i < count; i++) {
1615
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1616
+ const baseDelay = i * (1 / count) * 3;
1617
+ const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
1618
+ // Position based on particle type
1619
+ const isFirework = type === 'firework';
1620
+ const x = isFirework
1621
+ ? viewport.width * 0.2 + Math.random() * viewport.width * 0.6
1622
+ : Math.random() * viewport.width;
1623
+ const y = isFirework
1624
+ ? viewport.height * 0.2 + Math.random() * viewport.height * 0.4
1625
+ : viewport.height + 50 + Math.random() * 80;
1626
+ particles.push({
1627
+ id: `chinesenewyear-${i}-${Math.random().toString(36).substring(2, 9)}`,
1628
+ x,
1629
+ y,
1630
+ size,
1631
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1632
+ type,
1633
+ delay: baseDelay + Math.random() * 0.5,
1634
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1635
+ custom: {
1636
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1637
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1638
+ angle: Math.random() * Math.PI * 2,
1639
+ distance: 80 + Math.random() * 120,
1640
+ },
1641
+ });
1642
+ }
1643
+ return particles;
1644
+ }
1645
+ /**
1646
+ * Chinese New Year Effect Component
1647
+ * Renders rising red lanterns and golden fireworks
1648
+ * Red/gold color palette
1649
+ */
1650
+ const ChineseNewYearEffect = ({ particles, viewport }) => {
1651
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(ChineseNewYearParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
1652
+ };
1653
+
1654
+ /**
1655
+ * Holi effect configuration
1656
+ */
1657
+ const HOLI_CONFIG = {
1658
+ baseParticleCount: 50,
1659
+ colors: ['#FF1493', '#00FF00', '#FFFF00', '#FF4500', '#9400D3', '#00BFFF'],
1660
+ particleTypes: ['color-splash'],
1661
+ animationType: 'scatter',
1662
+ physics: {
1663
+ speed: 4,
1664
+ drift: 150,
1665
+ rotation: 180,
1666
+ scale: [0.4, 2.0],
1667
+ },
1668
+ };
1669
+ /**
1670
+ * Individual color splash particle with scatter animation
1671
+ */
1672
+ const HoliParticle = ({ particle }) => {
1673
+ const { angle, distance, rotation } = particle.custom;
1674
+ return (jsx(motion.div, { style: {
1675
+ position: 'absolute',
1676
+ left: particle.x,
1677
+ top: particle.y,
1678
+ width: particle.size,
1679
+ height: particle.size,
1680
+ color: particle.color,
1681
+ pointerEvents: 'none',
1682
+ filter: `drop-shadow(0 0 10px ${particle.color})`,
1683
+ }, initial: { scale: 0, opacity: 0.8 }, animate: {
1684
+ x: Math.cos(angle) * distance,
1685
+ y: Math.sin(angle) * distance + 50,
1686
+ scale: [0, 2, 0],
1687
+ opacity: [0.8, 0.6, 0],
1688
+ rotate: rotation,
1689
+ }, transition: {
1690
+ duration: particle.duration * 0.7,
1691
+ ease: 'easeOut',
1692
+ repeat: Infinity,
1693
+ repeatDelay: particle.duration * 0.3,
1694
+ delay: particle.delay,
1695
+ }, dangerouslySetInnerHTML: { __html: PARTICLE_SVGS['color-splash'] } }));
1696
+ };
1697
+ /**
1698
+ * Generate particles for Holi effect
1699
+ */
1700
+ function generateHoliParticles(count, viewport, colors) {
1701
+ const effectColors = colors && colors.length > 0 ? colors : HOLI_CONFIG.colors;
1702
+ const physics = HOLI_CONFIG.physics;
1703
+ const particles = [];
1704
+ for (let i = 0; i < count; i++) {
1705
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1706
+ const baseDelay = i * (1 / count) * 3;
1707
+ particles.push({
1708
+ id: `holi-${i}-${Math.random().toString(36).substring(2, 9)}`,
1709
+ x: viewport.width * 0.3 + Math.random() * viewport.width * 0.4,
1710
+ y: viewport.height * 0.3 + Math.random() * viewport.height * 0.4,
1711
+ size,
1712
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1713
+ type: 'color-splash',
1714
+ delay: baseDelay + Math.random() * 0.5,
1715
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1716
+ custom: {
1717
+ angle: Math.random() * Math.PI * 2,
1718
+ distance: 50 + Math.random() * 100,
1719
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1720
+ },
1721
+ });
1722
+ }
1723
+ return particles;
1724
+ }
1725
+ /**
1726
+ * Holi Effect Component
1727
+ * Renders scattered color powder bursts
1728
+ * Vibrant multi-color palette
1729
+ */
1730
+ const HoliEffect = ({ particles }) => {
1731
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(HoliParticle, { particle: particle }, particle.id))) }));
1732
+ };
1733
+
1734
+ /**
1735
+ * Eid effect configuration
1736
+ */
1737
+ const EID_CONFIG = {
1738
+ baseParticleCount: 35,
1739
+ colors: ['#FFD700', '#C0C0C0', '#228B22', '#FFFFFF'],
1740
+ particleTypes: ['moon', 'star'],
1741
+ animationType: 'float',
1742
+ physics: {
1743
+ speed: 0.8,
1744
+ drift: 10,
1745
+ rotation: 5,
1746
+ scale: [0.5, 1.5],
1747
+ },
1748
+ };
1749
+ /**
1750
+ * Individual moon/star particle with gentle floating animation
1751
+ */
1752
+ const EidParticle = ({ particle }) => {
1753
+ const { drift, rotation } = particle.custom;
1754
+ const svgString = PARTICLE_SVGS[particle.type];
1755
+ // Stars twinkle, moons glow
1756
+ const isStar = particle.type === 'star';
1757
+ const isMoon = particle.type === 'moon';
1758
+ return (jsx(motion.div, { style: {
1759
+ position: 'absolute',
1760
+ left: particle.x,
1761
+ top: particle.y,
1762
+ width: particle.size,
1763
+ height: particle.size,
1764
+ color: particle.color,
1765
+ pointerEvents: 'none',
1766
+ filter: isMoon
1767
+ ? 'drop-shadow(0 0 8px #FFD700)'
1768
+ : isStar
1769
+ ? 'drop-shadow(0 0 4px #FFFFFF)'
1770
+ : undefined,
1771
+ }, initial: { opacity: 0, scale: 0.8 }, animate: {
1772
+ x: [0, drift, -drift, 0],
1773
+ y: [0, -15, 8, 0],
1774
+ opacity: isStar ? [0, 1, 0.5, 1, 0] : [0, 1, 1, 0],
1775
+ scale: isStar ? [0.8, 1, 0.9, 1.1, 0.8] : [0.8, 1, 1.05, 1, 0.8],
1776
+ rotate: [0, rotation / 4, -rotation / 4, 0],
1777
+ }, transition: {
1778
+ duration: particle.duration,
1779
+ ease: 'easeInOut',
1780
+ repeat: Infinity,
1781
+ delay: particle.delay,
1782
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1783
+ };
1784
+ /**
1785
+ * Generate particles for Eid effect
1786
+ */
1787
+ function generateEidParticles(count, viewport, colors) {
1788
+ const effectColors = colors && colors.length > 0 ? colors : EID_CONFIG.colors;
1789
+ const physics = EID_CONFIG.physics;
1790
+ const particles = [];
1791
+ const particleTypes = EID_CONFIG.particleTypes;
1792
+ for (let i = 0; i < count; i++) {
1793
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1794
+ const baseDelay = i * (1 / count) * 3;
1795
+ const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
1796
+ particles.push({
1797
+ id: `eid-${i}-${Math.random().toString(36).substring(2, 9)}`,
1798
+ x: Math.random() * viewport.width,
1799
+ y: Math.random() * viewport.height,
1800
+ size,
1801
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1802
+ type,
1803
+ delay: baseDelay + Math.random() * 0.5,
1804
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1805
+ custom: {
1806
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1807
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1808
+ },
1809
+ });
1810
+ }
1811
+ return particles;
1812
+ }
1813
+ /**
1814
+ * Eid Effect Component
1815
+ * Renders floating crescent moons and twinkling stars
1816
+ * Gold/silver/green color palette
1817
+ */
1818
+ const EidEffect = ({ particles }) => {
1819
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(EidParticle, { particle: particle }, particle.id))) }));
1820
+ };
1821
+
1822
+ /**
1823
+ * St. Patrick's effect configuration
1824
+ */
1825
+ const STPATRICKS_CONFIG = {
1826
+ baseParticleCount: 40,
1827
+ colors: ['#228B22', '#32CD32', '#00FF00', '#FFD700'],
1828
+ particleTypes: ['shamrock', 'rainbow'],
1829
+ animationType: 'fall',
1830
+ physics: {
1831
+ speed: 2,
1832
+ drift: 40,
1833
+ rotation: 180,
1834
+ scale: [0.6, 1.3],
1835
+ },
1836
+ };
1837
+ /**
1838
+ * Individual shamrock/rainbow particle with spinning fall animation
1839
+ */
1840
+ const StPatricksParticle = ({ particle, viewportHeight }) => {
1841
+ const { drift, rotation } = particle.custom;
1842
+ const svgString = PARTICLE_SVGS[particle.type];
1843
+ // Shamrocks spin more, rainbows drift gently
1844
+ const isShamrock = particle.type === 'shamrock';
1845
+ return (jsx(motion.div, { style: {
1846
+ position: 'absolute',
1847
+ left: particle.x,
1848
+ width: particle.size,
1849
+ height: particle.size,
1850
+ color: particle.color,
1851
+ pointerEvents: 'none',
1852
+ }, initial: { y: -50, opacity: 0, rotate: 0 }, animate: {
1853
+ y: viewportHeight + 50,
1854
+ x: isShamrock ? [0, drift * 0.5, -drift * 0.3, drift] : drift,
1855
+ opacity: [0, 1, 1, 0],
1856
+ rotate: isShamrock ? rotation * 2 : rotation * 0.5,
1857
+ }, transition: {
1858
+ duration: particle.duration,
1859
+ ease: 'linear',
1860
+ repeat: Infinity,
1861
+ delay: particle.delay,
1862
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1863
+ };
1864
+ /**
1865
+ * Generate particles for St. Patrick's effect
1866
+ */
1867
+ function generateStPatricksParticles(count, viewport, colors) {
1868
+ const effectColors = colors && colors.length > 0 ? colors : STPATRICKS_CONFIG.colors;
1869
+ const physics = STPATRICKS_CONFIG.physics;
1870
+ const particles = [];
1871
+ for (let i = 0; i < count; i++) {
1872
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1873
+ const baseDelay = i * (1 / count) * 3;
1874
+ // More shamrocks than rainbows
1875
+ const type = Math.random() < 0.8 ? 'shamrock' : 'rainbow';
1876
+ particles.push({
1877
+ id: `stpatricks-${i}-${Math.random().toString(36).substring(2, 9)}`,
1878
+ x: Math.random() * viewport.width,
1879
+ y: -50 - Math.random() * 80,
1880
+ size,
1881
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1882
+ type,
1883
+ delay: baseDelay + Math.random() * 0.5,
1884
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1885
+ custom: {
1886
+ drift: physics.drift * (Math.random() - 0.5) * 2,
1887
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1888
+ },
1889
+ });
1890
+ }
1891
+ return particles;
1892
+ }
1893
+ /**
1894
+ * St. Patrick's Effect Component
1895
+ * Renders falling shamrocks with spin
1896
+ * Green color palette with gold accents
1897
+ */
1898
+ const StPatricksEffect = ({ particles, viewport }) => {
1899
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(StPatricksParticle, { particle: particle, viewportHeight: viewport.height }, particle.id))) }));
1900
+ };
1901
+
1902
+ /**
1903
+ * Independence Day effect configuration
1904
+ */
1905
+ const INDEPENDENCE_CONFIG = {
1906
+ baseParticleCount: 45,
1907
+ colors: ['#FF0000', '#FFFFFF', '#0000FF'], // Default US colors, customizable
1908
+ particleTypes: ['firework', 'spark', 'confetti'],
1909
+ animationType: 'explode',
1910
+ physics: {
1911
+ speed: 5,
1912
+ drift: 120,
1913
+ rotation: 360,
1914
+ scale: [0.3, 1.5],
1915
+ },
1916
+ };
1917
+ /**
1918
+ * Individual firework/spark/confetti particle with explode animation
1919
+ */
1920
+ const IndependenceParticle = ({ particle }) => {
1921
+ const { angle, distance, rotation } = particle.custom;
1922
+ const svgString = PARTICLE_SVGS[particle.type];
1923
+ // Fireworks have bigger explosions, confetti falls after burst
1924
+ const isFirework = particle.type === 'firework';
1925
+ const isConfetti = particle.type === 'confetti';
1926
+ return (jsx(motion.div, { style: {
1927
+ position: 'absolute',
1928
+ left: particle.x,
1929
+ top: particle.y,
1930
+ width: particle.size,
1931
+ height: particle.size,
1932
+ color: particle.color,
1933
+ pointerEvents: 'none',
1934
+ filter: isFirework ? `drop-shadow(0 0 6px ${particle.color})` : undefined,
1935
+ }, initial: { scale: 0, opacity: 1 }, animate: {
1936
+ x: Math.cos(angle) * distance * (isFirework ? 1.2 : 1),
1937
+ y: isConfetti
1938
+ ? [Math.sin(angle) * distance * 0.5, Math.sin(angle) * distance + 100]
1939
+ : Math.sin(angle) * distance,
1940
+ scale: isFirework ? [0, 1.8, 0] : [0, 1.5, 0],
1941
+ opacity: [1, 1, 0],
1942
+ rotate: isConfetti ? rotation * 2 : rotation,
1943
+ }, transition: {
1944
+ duration: particle.duration * (isConfetti ? 0.7 : 0.5),
1945
+ ease: isConfetti ? 'easeIn' : [0.25, 0.46, 0.45, 0.94],
1946
+ repeat: Infinity,
1947
+ repeatDelay: particle.duration * (isConfetti ? 0.3 : 0.5),
1948
+ delay: particle.delay,
1949
+ }, dangerouslySetInnerHTML: { __html: svgString } }));
1950
+ };
1951
+ /**
1952
+ * Generate particles for Independence Day effect
1953
+ */
1954
+ function generateIndependenceParticles(count, viewport, colors) {
1955
+ const effectColors = colors && colors.length > 0 ? colors : INDEPENDENCE_CONFIG.colors;
1956
+ const physics = INDEPENDENCE_CONFIG.physics;
1957
+ const particles = [];
1958
+ const particleTypes = INDEPENDENCE_CONFIG.particleTypes;
1959
+ for (let i = 0; i < count; i++) {
1960
+ const size = physics.scale[0] * 20 + Math.random() * (physics.scale[1] - physics.scale[0]) * 20;
1961
+ const baseDelay = i * (1 / count) * 3;
1962
+ const type = particleTypes[Math.floor(Math.random() * particleTypes.length)];
1963
+ particles.push({
1964
+ id: `independence-${i}-${Math.random().toString(36).substring(2, 9)}`,
1965
+ x: viewport.width * 0.2 + Math.random() * viewport.width * 0.6,
1966
+ y: viewport.height * 0.2 + Math.random() * viewport.height * 0.4,
1967
+ size,
1968
+ color: effectColors[Math.floor(Math.random() * effectColors.length)],
1969
+ type,
1970
+ delay: baseDelay + Math.random() * 0.5,
1971
+ duration: 10 / physics.speed + (Math.random() - 0.5) * 2,
1972
+ custom: {
1973
+ angle: Math.random() * Math.PI * 2,
1974
+ distance: 100 + Math.random() * 150,
1975
+ rotation: physics.rotation * (Math.random() - 0.5) * 2,
1976
+ },
1977
+ });
1978
+ }
1979
+ return particles;
1980
+ }
1981
+ /**
1982
+ * Independence Day Effect Component
1983
+ * Renders customizable color fireworks and patriotic confetti
1984
+ * Default US colors (red, white, blue), customizable via colors prop
1985
+ */
1986
+ const IndependenceEffect = ({ particles }) => {
1987
+ return (jsx(Fragment, { children: particles.map((particle) => (jsx(IndependenceParticle, { particle: particle }, particle.id))) }));
1988
+ };
1989
+
1990
+ // Individual Festival Effects
1991
+ // Each effect provides dedicated components and particle generators
1992
+ // Christmas - Falling snowflakes
1993
+ const EFFECT_GENERATORS = {
1994
+ christmas: generateChristmasParticles,
1995
+ newyear: generateNewYearParticles,
1996
+ valentine: generateValentineParticles,
1997
+ easter: generateEasterParticles,
1998
+ halloween: generateHalloweenParticles,
1999
+ thanksgiving: generateThanksgivingParticles,
2000
+ diwali: generateDiwaliParticles,
2001
+ chinesenewyear: generateChineseNewYearParticles,
2002
+ holi: generateHoliParticles,
2003
+ eid: generateEidParticles,
2004
+ stpatricks: generateStPatricksParticles,
2005
+ independence: generateIndependenceParticles,
2006
+ };
2007
+
2008
+ export { ANIMATION_VARIANTS, CHINESENEWYEAR_CONFIG, CHRISTMAS_CONFIG, ChineseNewYearEffect, ChristmasEffect, DIWALI_CONFIG, DiwaliEffect, EASTER_CONFIG, EFFECT_GENERATORS, EID_CONFIG, EasterEffect, EidEffect, FESTIVAL_CONFIGS, FESTIVAL_TYPES, FestivalRegistry, FestiveEffects, HALLOWEEN_CONFIG, HOLI_CONFIG, HalloweenEffect, HoliEffect, INDEPENDENCE_CONFIG, INTENSITY_CONFIG, IndependenceEffect, NEWYEAR_CONFIG, NewYearEffect, PARTICLE_SVGS, Particle, STPATRICKS_CONFIG, StPatricksEffect, THANKSGIVING_CONFIG, ThanksgivingEffect, VALENTINE_CONFIG, ValentineEffect, calculateParticleCount, createAnimationCustomData, createFestivalEffect, defaultRegistry, explodeVariants, fallVariants, floatVariants, generateChineseNewYearParticles, generateChristmasParticles, generateDiwaliParticles, generateEasterParticles, generateEidParticles, generateHalloweenParticles, generateHoliParticles, generateIndependenceParticles, generateNewYearParticles, generateParticles, generateStPatricksParticles, generateThanksgivingParticles, generateValentineParticles, getAnimationPropsForType, getExplodeTransition, getFallTransition, getFloatTransition, getParticleCount, getRiseTransition, getScatterTransition, getTransitionForType, riseVariants, scatterVariants, useReducedMotion };
2009
+ //# sourceMappingURL=index.esm.js.map