react-native-puff-pop 1.0.6 → 1.0.8

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.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState, useCallback, useMemo, Children, } from 'react';
2
+ import { useEffect, useRef, useState, useCallback, useMemo, memo, Children, } from 'react';
3
3
  import { View, Animated, StyleSheet, Easing, AccessibilityInfo, } from 'react-native';
4
4
  /**
5
5
  * Get easing function based on type
@@ -22,16 +22,160 @@ function getEasing(type) {
22
22
  return Easing.out(Easing.ease);
23
23
  }
24
24
  }
25
+ /**
26
+ * Get effect flags for any effect type
27
+ * Returns flags indicating which transforms are needed for the effect
28
+ */
29
+ function getEffectFlags(eff) {
30
+ return {
31
+ hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip', 'pulse', 'elastic'].includes(eff),
32
+ hasRotate: ['rotate', 'rotateScale', 'swing', 'wobble'].includes(eff),
33
+ hasFlip: eff === 'flip',
34
+ hasTranslateX: ['slideLeft', 'slideRight', 'shake', 'wobble'].includes(eff),
35
+ hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(eff),
36
+ hasRotateEffect: ['rotate', 'rotateScale', 'flip', 'swing', 'wobble'].includes(eff),
37
+ // Special effects that need sequence animation
38
+ isShake: eff === 'shake',
39
+ isPulse: eff === 'pulse',
40
+ isSwing: eff === 'swing',
41
+ isWobble: eff === 'wobble',
42
+ isElastic: eff === 'elastic',
43
+ };
44
+ }
45
+ /**
46
+ * Get anchor point offset multipliers
47
+ * Returns { x: -1 to 1, y: -1 to 1 } where 0 is center
48
+ */
49
+ function getAnchorPointOffset(anchorPoint) {
50
+ switch (anchorPoint) {
51
+ case 'top':
52
+ return { x: 0, y: -0.5 };
53
+ case 'bottom':
54
+ return { x: 0, y: 0.5 };
55
+ case 'left':
56
+ return { x: -0.5, y: 0 };
57
+ case 'right':
58
+ return { x: 0.5, y: 0 };
59
+ case 'topLeft':
60
+ return { x: -0.5, y: -0.5 };
61
+ case 'topRight':
62
+ return { x: 0.5, y: -0.5 };
63
+ case 'bottomLeft':
64
+ return { x: -0.5, y: 0.5 };
65
+ case 'bottomRight':
66
+ return { x: 0.5, y: 0.5 };
67
+ case 'center':
68
+ default:
69
+ return { x: 0, y: 0 };
70
+ }
71
+ }
72
+ /**
73
+ * Props comparison function for PuffPop memoization
74
+ * Performs shallow comparison of props to prevent unnecessary re-renders
75
+ */
76
+ function arePuffPopPropsEqual(prevProps, nextProps) {
77
+ // Compare primitive props
78
+ if (prevProps.effect !== nextProps.effect ||
79
+ prevProps.duration !== nextProps.duration ||
80
+ prevProps.delay !== nextProps.delay ||
81
+ prevProps.easing !== nextProps.easing ||
82
+ prevProps.skeleton !== nextProps.skeleton ||
83
+ prevProps.visible !== nextProps.visible ||
84
+ prevProps.animateOnMount !== nextProps.animateOnMount ||
85
+ prevProps.loop !== nextProps.loop ||
86
+ prevProps.loopDelay !== nextProps.loopDelay ||
87
+ prevProps.respectReduceMotion !== nextProps.respectReduceMotion ||
88
+ prevProps.testID !== nextProps.testID ||
89
+ prevProps.reverse !== nextProps.reverse ||
90
+ prevProps.intensity !== nextProps.intensity ||
91
+ prevProps.anchorPoint !== nextProps.anchorPoint ||
92
+ prevProps.useSpring !== nextProps.useSpring ||
93
+ prevProps.exitEffect !== nextProps.exitEffect ||
94
+ prevProps.exitDuration !== nextProps.exitDuration ||
95
+ prevProps.exitEasing !== nextProps.exitEasing ||
96
+ prevProps.exitDelay !== nextProps.exitDelay ||
97
+ prevProps.initialOpacity !== nextProps.initialOpacity ||
98
+ prevProps.initialScale !== nextProps.initialScale ||
99
+ prevProps.initialRotate !== nextProps.initialRotate ||
100
+ prevProps.initialTranslateX !== nextProps.initialTranslateX ||
101
+ prevProps.initialTranslateY !== nextProps.initialTranslateY) {
102
+ return false;
103
+ }
104
+ // Compare springConfig object (shallow)
105
+ if (prevProps.springConfig !== nextProps.springConfig) {
106
+ if (!prevProps.springConfig ||
107
+ !nextProps.springConfig ||
108
+ prevProps.springConfig.tension !== nextProps.springConfig.tension ||
109
+ prevProps.springConfig.friction !== nextProps.springConfig.friction ||
110
+ prevProps.springConfig.speed !== nextProps.springConfig.speed ||
111
+ prevProps.springConfig.bounciness !== nextProps.springConfig.bounciness) {
112
+ return false;
113
+ }
114
+ }
115
+ // Compare callbacks (reference equality - if changed, should re-render)
116
+ if (prevProps.onAnimationStart !== nextProps.onAnimationStart ||
117
+ prevProps.onAnimationComplete !== nextProps.onAnimationComplete) {
118
+ return false;
119
+ }
120
+ // Style comparison - if style prop changes, re-render
121
+ // Note: Deep comparison of style is expensive, so we use reference equality
122
+ if (prevProps.style !== nextProps.style) {
123
+ return false;
124
+ }
125
+ // Children comparison - if children change, re-render
126
+ if (prevProps.children !== nextProps.children) {
127
+ return false;
128
+ }
129
+ return true;
130
+ }
25
131
  /**
26
132
  * PuffPop - Animate children with beautiful entrance effects
27
133
  */
28
- export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0, easing = 'easeOut', skeleton = true, visible = true, onAnimationComplete, onAnimationStart, style, animateOnMount = true, loop = false, loopDelay = 0, respectReduceMotion = true, testID, }) {
134
+ function PuffPopComponent({ children, effect = 'scale', duration = 400, delay = 0, easing = 'easeOut', skeleton = true, visible = true, onAnimationComplete, onAnimationStart, style, animateOnMount = true, loop = false, loopDelay = 0, respectReduceMotion = true, testID,
135
+ // Exit animation settings
136
+ exitEffect, exitDuration, exitEasing, exitDelay = 0,
137
+ // Custom initial values
138
+ initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY,
139
+ // Reverse mode
140
+ reverse = false,
141
+ // Animation intensity
142
+ intensity = 1,
143
+ // Anchor point
144
+ anchorPoint = 'center',
145
+ // Spring animation
146
+ useSpring = false, springConfig, }) {
147
+ // Clamp intensity between 0 and 1
148
+ const clampedIntensity = Math.max(0, Math.min(1, intensity));
149
+ // Helper to get initial value with custom override, reverse, and intensity support
150
+ const getInitialOpacityValue = () => initialOpacity ?? 0;
151
+ const getInitialScaleValue = (eff) => {
152
+ if (initialScale !== undefined)
153
+ return initialScale;
154
+ const baseScale = getInitialScale(eff, reverse);
155
+ // Scale goes from baseScale to 1, so we interpolate: 1 - (1 - baseScale) * intensity
156
+ return 1 - (1 - baseScale) * clampedIntensity;
157
+ };
158
+ const getInitialRotateValue = (eff) => {
159
+ if (initialRotate !== undefined)
160
+ return initialRotate;
161
+ return getInitialRotate(eff, reverse) * clampedIntensity;
162
+ };
163
+ const getInitialTranslateXValue = (eff) => {
164
+ if (initialTranslateX !== undefined)
165
+ return initialTranslateX;
166
+ return getInitialTranslateX(eff, reverse) * clampedIntensity;
167
+ };
168
+ const getInitialTranslateYValue = (eff) => {
169
+ if (initialTranslateY !== undefined)
170
+ return initialTranslateY;
171
+ return getInitialTranslateY(eff, reverse) * clampedIntensity;
172
+ };
29
173
  // Animation values
30
- const opacity = useRef(new Animated.Value(animateOnMount ? 0 : 1)).current;
31
- const scale = useRef(new Animated.Value(animateOnMount ? getInitialScale(effect) : 1)).current;
32
- const rotate = useRef(new Animated.Value(animateOnMount ? getInitialRotate(effect) : 0)).current;
33
- const translateX = useRef(new Animated.Value(animateOnMount ? getInitialTranslateX(effect) : 0)).current;
34
- const translateY = useRef(new Animated.Value(animateOnMount ? getInitialTranslateY(effect) : 0)).current;
174
+ const opacity = useRef(new Animated.Value(animateOnMount ? getInitialOpacityValue() : 1)).current;
175
+ const scale = useRef(new Animated.Value(animateOnMount ? getInitialScaleValue(effect) : 1)).current;
176
+ const rotate = useRef(new Animated.Value(animateOnMount ? getInitialRotateValue(effect) : 0)).current;
177
+ const translateX = useRef(new Animated.Value(animateOnMount ? getInitialTranslateXValue(effect) : 0)).current;
178
+ const translateY = useRef(new Animated.Value(animateOnMount ? getInitialTranslateYValue(effect) : 0)).current;
35
179
  // For non-skeleton mode
36
180
  const [measuredHeight, setMeasuredHeight] = useState(null);
37
181
  const animatedHeight = useRef(new Animated.Value(0)).current;
@@ -55,15 +199,11 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
55
199
  }, [respectReduceMotion]);
56
200
  // Effective duration (0 if reduce motion is enabled)
57
201
  const effectiveDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : duration;
202
+ const effectiveExitDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : (exitDuration ?? duration);
58
203
  // Memoize effect type checks to avoid repeated includes() calls
59
- const effectFlags = useMemo(() => ({
60
- hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(effect),
61
- hasRotate: ['rotate', 'rotateScale'].includes(effect),
62
- hasFlip: effect === 'flip',
63
- hasTranslateX: ['slideLeft', 'slideRight'].includes(effect),
64
- hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(effect),
65
- hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(effect),
66
- }), [effect]);
204
+ const effectFlags = useMemo(() => getEffectFlags(effect), [effect]);
205
+ // Exit effect flags (use exitEffect if specified, otherwise same as enter effect)
206
+ const exitEffectFlags = useMemo(() => exitEffect ? getEffectFlags(exitEffect) : effectFlags, [exitEffect, effectFlags]);
67
207
  // Memoize interpolations to avoid recreating on every render
68
208
  const rotateInterpolation = useMemo(() => rotate.interpolate({
69
209
  inputRange: [-360, 0, 360],
@@ -86,60 +226,102 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
86
226
  if (toVisible && onAnimationStart) {
87
227
  onAnimationStart();
88
228
  }
89
- const easingFn = getEasing(easing);
229
+ // Determine which effect settings to use based on direction
230
+ const currentEffect = toVisible ? effect : (exitEffect ?? effect);
231
+ const currentDuration = toVisible ? effectiveDuration : effectiveExitDuration;
232
+ const currentEasing = toVisible ? easing : (exitEasing ?? easing);
233
+ const currentDelay = toVisible ? delay : exitDelay;
234
+ const currentFlags = toVisible ? effectFlags : exitEffectFlags;
235
+ const easingFn = getEasing(currentEasing);
90
236
  // When skeleton is false, we animate height which doesn't support native driver
91
237
  // So we must use JS driver for all animations in that case
92
238
  const useNative = skeleton;
93
- const config = {
94
- duration: effectiveDuration,
239
+ // Spring configuration
240
+ const springConf = {
241
+ tension: springConfig?.tension ?? 100,
242
+ friction: springConfig?.friction ?? 10,
243
+ speed: springConfig?.speed,
244
+ bounciness: springConfig?.bounciness,
245
+ useNativeDriver: useNative,
246
+ };
247
+ const timingConfig = {
248
+ duration: currentDuration,
95
249
  easing: easingFn,
96
250
  useNativeDriver: useNative,
97
251
  };
252
+ // Helper to create animation (spring or timing)
253
+ const createAnimation = (value, toValue, customEasing) => {
254
+ if (useSpring) {
255
+ return Animated.spring(value, {
256
+ toValue,
257
+ ...springConf,
258
+ });
259
+ }
260
+ return Animated.timing(value, {
261
+ toValue,
262
+ ...timingConfig,
263
+ ...(customEasing ? { easing: customEasing } : {}),
264
+ });
265
+ };
98
266
  const animations = [];
99
- // Opacity animation
267
+ // Opacity animation (always use timing for opacity for smoother fade)
100
268
  animations.push(Animated.timing(opacity, {
101
269
  toValue: toVisible ? 1 : 0,
102
- ...config,
270
+ ...timingConfig,
103
271
  }));
104
272
  // Scale animation
105
- if (effectFlags.hasScale) {
106
- const targetScale = toVisible ? 1 : getInitialScale(effect);
107
- animations.push(Animated.timing(scale, {
108
- toValue: targetScale,
109
- ...config,
110
- easing: effect === 'bounce' ? Easing.bounce : easingFn,
111
- }));
273
+ if (currentFlags.hasScale) {
274
+ const targetScale = toVisible ? 1 : getInitialScaleValue(currentEffect);
275
+ // Special easing for different effects
276
+ let scaleEasing = easingFn;
277
+ if (currentEffect === 'bounce') {
278
+ scaleEasing = Easing.bounce;
279
+ }
280
+ else if (currentEffect === 'elastic') {
281
+ scaleEasing = Easing.elastic(1.5);
282
+ }
283
+ else if (currentEffect === 'pulse') {
284
+ scaleEasing = Easing.out(Easing.back(3));
285
+ }
286
+ animations.push(createAnimation(scale, targetScale, scaleEasing));
112
287
  }
113
288
  // Rotate animation
114
- if (effectFlags.hasRotate || effectFlags.hasFlip) {
115
- const targetRotate = toVisible ? 0 : getInitialRotate(effect);
116
- animations.push(Animated.timing(rotate, {
117
- toValue: targetRotate,
118
- ...config,
119
- }));
289
+ if (currentFlags.hasRotate || currentFlags.hasFlip) {
290
+ const targetRotate = toVisible ? 0 : getInitialRotateValue(currentEffect);
291
+ // Special easing for swing and wobble
292
+ let rotateEasing = easingFn;
293
+ if (currentEffect === 'swing') {
294
+ rotateEasing = Easing.elastic(1.2);
295
+ }
296
+ else if (currentEffect === 'wobble') {
297
+ rotateEasing = Easing.elastic(1.5);
298
+ }
299
+ animations.push(createAnimation(rotate, targetRotate, rotateEasing));
120
300
  }
121
301
  // TranslateX animation
122
- if (effectFlags.hasTranslateX) {
123
- const targetX = toVisible ? 0 : getInitialTranslateX(effect);
124
- animations.push(Animated.timing(translateX, {
125
- toValue: targetX,
126
- ...config,
127
- }));
302
+ if (currentFlags.hasTranslateX) {
303
+ const targetX = toVisible ? 0 : getInitialTranslateXValue(currentEffect);
304
+ // Special easing for shake and wobble
305
+ let translateXEasing = easingFn;
306
+ if (currentEffect === 'shake') {
307
+ translateXEasing = Easing.elastic(3);
308
+ }
309
+ else if (currentEffect === 'wobble') {
310
+ translateXEasing = Easing.elastic(1.5);
311
+ }
312
+ animations.push(createAnimation(translateX, targetX, translateXEasing));
128
313
  }
129
314
  // TranslateY animation
130
- if (effectFlags.hasTranslateY) {
131
- const targetY = toVisible ? 0 : getInitialTranslateY(effect);
132
- animations.push(Animated.timing(translateY, {
133
- toValue: targetY,
134
- ...config,
135
- }));
315
+ if (currentFlags.hasTranslateY) {
316
+ const targetY = toVisible ? 0 : getInitialTranslateYValue(currentEffect);
317
+ animations.push(createAnimation(translateY, targetY));
136
318
  }
137
- // Height animation for non-skeleton mode
319
+ // Height animation for non-skeleton mode (always use timing)
138
320
  if (!skeleton && measuredHeight !== null) {
139
321
  const targetHeight = toVisible ? measuredHeight : 0;
140
322
  animations.push(Animated.timing(animatedHeight, {
141
323
  toValue: targetHeight,
142
- duration: effectiveDuration,
324
+ duration: currentDuration,
143
325
  easing: easingFn,
144
326
  useNativeDriver: false,
145
327
  }));
@@ -148,20 +330,20 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
148
330
  const parallelAnimation = Animated.parallel(animations);
149
331
  // Reset values function for looping
150
332
  const resetValues = () => {
151
- opacity.setValue(0);
152
- scale.setValue(getInitialScale(effect));
153
- rotate.setValue(getInitialRotate(effect));
154
- translateX.setValue(getInitialTranslateX(effect));
155
- translateY.setValue(getInitialTranslateY(effect));
333
+ opacity.setValue(getInitialOpacityValue());
334
+ scale.setValue(getInitialScaleValue(effect));
335
+ rotate.setValue(getInitialRotateValue(effect));
336
+ translateX.setValue(getInitialTranslateXValue(effect));
337
+ translateY.setValue(getInitialTranslateYValue(effect));
156
338
  if (!skeleton && measuredHeight !== null) {
157
339
  animatedHeight.setValue(0);
158
340
  }
159
341
  };
160
342
  // Build the animation sequence
161
343
  let animation;
162
- if (delay > 0) {
344
+ if (currentDelay > 0) {
163
345
  animation = Animated.sequence([
164
- Animated.delay(delay),
346
+ Animated.delay(currentDelay),
165
347
  parallelAnimation,
166
348
  ]);
167
349
  }
@@ -230,9 +412,14 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
230
412
  }, [
231
413
  delay,
232
414
  effectiveDuration,
415
+ effectiveExitDuration,
233
416
  easing,
234
417
  effect,
235
418
  effectFlags,
419
+ exitEffect,
420
+ exitEffectFlags,
421
+ exitEasing,
422
+ exitDelay,
236
423
  measuredHeight,
237
424
  onAnimationComplete,
238
425
  onAnimationStart,
@@ -245,6 +432,8 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
245
432
  animatedHeight,
246
433
  loop,
247
434
  loopDelay,
435
+ useSpring,
436
+ springConfig,
248
437
  ]);
249
438
  // Handle initial mount animation
250
439
  useEffect(() => {
@@ -270,11 +459,33 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
270
459
  }
271
460
  };
272
461
  }, []);
462
+ // Calculate anchor point offset (using 100px as base size for skeleton mode)
463
+ const anchorOffset = useMemo(() => {
464
+ const offset = getAnchorPointOffset(anchorPoint);
465
+ // Use measured height if available, otherwise use 100px as base
466
+ const baseSize = measuredHeight ?? 100;
467
+ return {
468
+ x: offset.x * baseSize,
469
+ y: offset.y * baseSize,
470
+ };
471
+ }, [anchorPoint, measuredHeight]);
273
472
  // Memoize transform array to avoid recreating on every render
274
473
  // IMPORTANT: All hooks must be called before any conditional returns
275
474
  const transform = useMemo(() => {
276
475
  const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
476
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
277
477
  const transforms = [];
478
+ const needsAnchorOffset = anchorPoint !== 'center' && (hasScale || hasRotate || hasFlip);
479
+ // Step 1: Move to anchor point (negative offset)
480
+ if (needsAnchorOffset) {
481
+ if (anchorOffset.x !== 0) {
482
+ transforms.push({ translateX: -anchorOffset.x });
483
+ }
484
+ if (anchorOffset.y !== 0) {
485
+ transforms.push({ translateY: -anchorOffset.y });
486
+ }
487
+ }
488
+ // Step 2: Apply scale/rotate transforms
278
489
  if (hasScale) {
279
490
  transforms.push({ scale });
280
491
  }
@@ -284,6 +495,16 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
284
495
  if (hasFlip) {
285
496
  transforms.push({ rotateY: flipInterpolation });
286
497
  }
498
+ // Step 3: Move back from anchor point (positive offset)
499
+ if (needsAnchorOffset) {
500
+ if (anchorOffset.x !== 0) {
501
+ transforms.push({ translateX: anchorOffset.x });
502
+ }
503
+ if (anchorOffset.y !== 0) {
504
+ transforms.push({ translateY: anchorOffset.y });
505
+ }
506
+ }
507
+ // Step 4: Apply other translate transforms
287
508
  if (hasTranslateX) {
288
509
  transforms.push({ translateX });
289
510
  }
@@ -291,7 +512,7 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
291
512
  transforms.push({ translateY });
292
513
  }
293
514
  return transforms.length > 0 ? transforms : undefined;
294
- }, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
515
+ }, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY, anchorPoint, anchorOffset]);
295
516
  // Memoize animated style
296
517
  const animatedStyle = useMemo(() => ({
297
518
  opacity,
@@ -313,13 +534,17 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
313
534
  }
314
535
  return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], testID: testID, children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
315
536
  }
537
+ // Memoize PuffPop to prevent unnecessary re-renders
538
+ export const PuffPop = memo(PuffPopComponent, arePuffPopPropsEqual);
316
539
  /**
317
540
  * Get initial scale value based on effect
318
541
  */
319
- function getInitialScale(effect) {
542
+ function getInitialScale(effect, _reverse = false) {
543
+ // Scale doesn't change with reverse (parameter kept for consistent API)
320
544
  switch (effect) {
321
545
  case 'scale':
322
546
  case 'rotateScale':
547
+ case 'elastic':
323
548
  return 0;
324
549
  case 'bounce':
325
550
  return 0.3;
@@ -327,6 +552,8 @@ function getInitialScale(effect) {
327
552
  return 0.5;
328
553
  case 'flip':
329
554
  return 0.8;
555
+ case 'pulse':
556
+ return 0.6;
330
557
  default:
331
558
  return 1;
332
559
  }
@@ -334,14 +561,19 @@ function getInitialScale(effect) {
334
561
  /**
335
562
  * Get initial rotate value based on effect
336
563
  */
337
- function getInitialRotate(effect) {
564
+ function getInitialRotate(effect, reverse = false) {
565
+ const multiplier = reverse ? -1 : 1;
338
566
  switch (effect) {
339
567
  case 'rotate':
340
- return -360;
568
+ return -360 * multiplier;
341
569
  case 'rotateScale':
342
- return -180;
570
+ return -180 * multiplier;
343
571
  case 'flip':
344
- return -180;
572
+ return -180 * multiplier;
573
+ case 'swing':
574
+ return -15 * multiplier;
575
+ case 'wobble':
576
+ return -5 * multiplier;
345
577
  default:
346
578
  return 0;
347
579
  }
@@ -349,12 +581,17 @@ function getInitialRotate(effect) {
349
581
  /**
350
582
  * Get initial translateX value based on effect
351
583
  */
352
- function getInitialTranslateX(effect) {
584
+ function getInitialTranslateX(effect, reverse = false) {
585
+ const multiplier = reverse ? -1 : 1;
353
586
  switch (effect) {
354
587
  case 'slideLeft':
355
- return 100;
588
+ return 100 * multiplier;
356
589
  case 'slideRight':
357
- return -100;
590
+ return -100 * multiplier;
591
+ case 'shake':
592
+ return -10 * multiplier;
593
+ case 'wobble':
594
+ return -25 * multiplier;
358
595
  default:
359
596
  return 0;
360
597
  }
@@ -362,14 +599,15 @@ function getInitialTranslateX(effect) {
362
599
  /**
363
600
  * Get initial translateY value based on effect
364
601
  */
365
- function getInitialTranslateY(effect) {
602
+ function getInitialTranslateY(effect, reverse = false) {
603
+ const multiplier = reverse ? -1 : 1;
366
604
  switch (effect) {
367
605
  case 'slideUp':
368
- return 50;
606
+ return 50 * multiplier;
369
607
  case 'slideDown':
370
- return -50;
608
+ return -50 * multiplier;
371
609
  case 'bounce':
372
- return 30;
610
+ return 30 * multiplier;
373
611
  default:
374
612
  return 0;
375
613
  }
@@ -386,6 +624,68 @@ const styles = StyleSheet.create({
386
624
  },
387
625
  groupContainer: {},
388
626
  });
627
+ /**
628
+ * Props comparison function for PuffPopGroup memoization
629
+ * Performs shallow comparison of props to prevent unnecessary re-renders
630
+ */
631
+ function arePuffPopGroupPropsEqual(prevProps, nextProps) {
632
+ // Compare primitive props
633
+ if (prevProps.effect !== nextProps.effect ||
634
+ prevProps.duration !== nextProps.duration ||
635
+ prevProps.staggerDelay !== nextProps.staggerDelay ||
636
+ prevProps.initialDelay !== nextProps.initialDelay ||
637
+ prevProps.easing !== nextProps.easing ||
638
+ prevProps.skeleton !== nextProps.skeleton ||
639
+ prevProps.visible !== nextProps.visible ||
640
+ prevProps.animateOnMount !== nextProps.animateOnMount ||
641
+ prevProps.respectReduceMotion !== nextProps.respectReduceMotion ||
642
+ prevProps.testID !== nextProps.testID ||
643
+ prevProps.staggerDirection !== nextProps.staggerDirection ||
644
+ prevProps.horizontal !== nextProps.horizontal ||
645
+ prevProps.gap !== nextProps.gap ||
646
+ prevProps.reverse !== nextProps.reverse ||
647
+ prevProps.intensity !== nextProps.intensity ||
648
+ prevProps.anchorPoint !== nextProps.anchorPoint ||
649
+ prevProps.useSpring !== nextProps.useSpring ||
650
+ prevProps.exitEffect !== nextProps.exitEffect ||
651
+ prevProps.exitDuration !== nextProps.exitDuration ||
652
+ prevProps.exitEasing !== nextProps.exitEasing ||
653
+ prevProps.exitDelay !== nextProps.exitDelay ||
654
+ prevProps.exitStaggerDelay !== nextProps.exitStaggerDelay ||
655
+ prevProps.exitStaggerDirection !== nextProps.exitStaggerDirection ||
656
+ prevProps.initialOpacity !== nextProps.initialOpacity ||
657
+ prevProps.initialScale !== nextProps.initialScale ||
658
+ prevProps.initialRotate !== nextProps.initialRotate ||
659
+ prevProps.initialTranslateX !== nextProps.initialTranslateX ||
660
+ prevProps.initialTranslateY !== nextProps.initialTranslateY) {
661
+ return false;
662
+ }
663
+ // Compare springConfig object (shallow)
664
+ if (prevProps.springConfig !== nextProps.springConfig) {
665
+ if (!prevProps.springConfig ||
666
+ !nextProps.springConfig ||
667
+ prevProps.springConfig.tension !== nextProps.springConfig.tension ||
668
+ prevProps.springConfig.friction !== nextProps.springConfig.friction ||
669
+ prevProps.springConfig.speed !== nextProps.springConfig.speed ||
670
+ prevProps.springConfig.bounciness !== nextProps.springConfig.bounciness) {
671
+ return false;
672
+ }
673
+ }
674
+ // Compare callbacks
675
+ if (prevProps.onAnimationStart !== nextProps.onAnimationStart ||
676
+ prevProps.onAnimationComplete !== nextProps.onAnimationComplete) {
677
+ return false;
678
+ }
679
+ // Style comparison
680
+ if (prevProps.style !== nextProps.style) {
681
+ return false;
682
+ }
683
+ // Children comparison - if children change, re-render
684
+ if (prevProps.children !== nextProps.children) {
685
+ return false;
686
+ }
687
+ return true;
688
+ }
389
689
  /**
390
690
  * PuffPopGroup - Animate multiple children with staggered entrance effects
391
691
  *
@@ -398,7 +698,19 @@ const styles = StyleSheet.create({
398
698
  * </PuffPopGroup>
399
699
  * ```
400
700
  */
401
- export function PuffPopGroup({ children, effect = 'scale', duration = 400, staggerDelay = 100, initialDelay = 0, easing = 'easeOut', skeleton = true, visible = true, animateOnMount = true, onAnimationComplete, onAnimationStart, style, respectReduceMotion = true, testID, staggerDirection = 'forward', horizontal = false, gap, }) {
701
+ function PuffPopGroupComponent({ children, effect = 'scale', duration = 400, staggerDelay = 100, initialDelay = 0, easing = 'easeOut', skeleton = true, visible = true, animateOnMount = true, onAnimationComplete, onAnimationStart, style, respectReduceMotion = true, testID, staggerDirection = 'forward', horizontal = false, gap,
702
+ // Custom initial values
703
+ initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY,
704
+ // Reverse mode
705
+ reverse,
706
+ // Animation intensity
707
+ intensity,
708
+ // Anchor point
709
+ anchorPoint,
710
+ // Spring animation
711
+ useSpring, springConfig,
712
+ // Exit animation settings
713
+ exitEffect, exitDuration, exitEasing, exitDelay, exitStaggerDelay = 0, exitStaggerDirection = 'reverse', }) {
402
714
  const childArray = Children.toArray(children);
403
715
  const childCount = childArray.length;
404
716
  const completedCount = useRef(0);
@@ -427,6 +739,34 @@ export function PuffPopGroup({ children, effect = 'scale', duration = 400, stagg
427
739
  }
428
740
  return initialDelay + delayIndex * staggerDelay;
429
741
  }, [childCount, initialDelay, staggerDelay, staggerDirection]);
742
+ // Calculate exit delay for each child based on exit stagger direction
743
+ const getChildExitDelay = useCallback((index) => {
744
+ if (exitStaggerDelay === 0) {
745
+ return exitDelay ?? 0;
746
+ }
747
+ let delayIndex;
748
+ switch (exitStaggerDirection) {
749
+ case 'forward':
750
+ delayIndex = index;
751
+ break;
752
+ case 'center': {
753
+ const center = (childCount - 1) / 2;
754
+ delayIndex = Math.abs(index - center);
755
+ break;
756
+ }
757
+ case 'edges': {
758
+ const center = (childCount - 1) / 2;
759
+ delayIndex = center - Math.abs(index - center);
760
+ break;
761
+ }
762
+ case 'reverse':
763
+ default:
764
+ // Reverse is default for exit (last in, first out)
765
+ delayIndex = childCount - 1 - index;
766
+ break;
767
+ }
768
+ return (exitDelay ?? 0) + delayIndex * exitStaggerDelay;
769
+ }, [childCount, exitDelay, exitStaggerDelay, exitStaggerDirection]);
430
770
  // Handle individual child animation complete
431
771
  const handleChildComplete = useCallback(() => {
432
772
  completedCount.current += 1;
@@ -457,6 +797,8 @@ export function PuffPopGroup({ children, effect = 'scale', duration = 400, stagg
457
797
  }
458
798
  return baseStyle;
459
799
  }, [horizontal, gap]);
460
- return (_jsx(View, { style: [styles.groupContainer, containerStyle, style], testID: testID, children: childArray.map((child, index) => (_jsx(PuffPop, { effect: effect, duration: duration, delay: getChildDelay(index), easing: easing, skeleton: skeleton, visible: visible, animateOnMount: animateOnMount, onAnimationComplete: handleChildComplete, onAnimationStart: index === 0 ? handleChildStart : undefined, respectReduceMotion: respectReduceMotion, children: child }, index))) }));
800
+ return (_jsx(View, { style: [styles.groupContainer, containerStyle, style], testID: testID, children: childArray.map((child, index) => (_jsx(PuffPop, { effect: effect, duration: duration, delay: getChildDelay(index), easing: easing, skeleton: skeleton, visible: visible, animateOnMount: animateOnMount, onAnimationComplete: handleChildComplete, onAnimationStart: index === 0 ? handleChildStart : undefined, respectReduceMotion: respectReduceMotion, initialOpacity: initialOpacity, initialScale: initialScale, initialRotate: initialRotate, initialTranslateX: initialTranslateX, initialTranslateY: initialTranslateY, reverse: reverse, intensity: intensity, anchorPoint: anchorPoint, useSpring: useSpring, springConfig: springConfig, exitEffect: exitEffect, exitDuration: exitDuration, exitEasing: exitEasing, exitDelay: getChildExitDelay(index), children: child }, index))) }));
461
801
  }
802
+ // Memoize PuffPopGroup to prevent unnecessary re-renders
803
+ export const PuffPopGroup = memo(PuffPopGroupComponent, arePuffPopGroupPropsEqual);
462
804
  export default PuffPop;