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