react-native-puff-pop 1.0.7 → 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,10 +22,116 @@ 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,
29
135
  // Exit animation settings
30
136
  exitEffect, exitDuration, exitEasing, exitDelay = 0,
31
137
  // Custom initial values
@@ -33,7 +139,11 @@ initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslate
33
139
  // Reverse mode
34
140
  reverse = false,
35
141
  // Animation intensity
36
- intensity = 1, }) {
142
+ intensity = 1,
143
+ // Anchor point
144
+ anchorPoint = 'center',
145
+ // Spring animation
146
+ useSpring = false, springConfig, }) {
37
147
  // Clamp intensity between 0 and 1
38
148
  const clampedIntensity = Math.max(0, Math.min(1, intensity));
39
149
  // Helper to get initial value with custom override, reverse, and intensity support
@@ -90,19 +200,10 @@ intensity = 1, }) {
90
200
  // Effective duration (0 if reduce motion is enabled)
91
201
  const effectiveDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : duration;
92
202
  const effectiveExitDuration = respectReduceMotion && isReduceMotionEnabled ? 0 : (exitDuration ?? duration);
93
- // Helper to get effect flags for any effect type
94
- const getEffectFlags = useCallback((eff) => ({
95
- hasScale: ['scale', 'bounce', 'zoom', 'rotateScale', 'flip'].includes(eff),
96
- hasRotate: ['rotate', 'rotateScale'].includes(eff),
97
- hasFlip: eff === 'flip',
98
- hasTranslateX: ['slideLeft', 'slideRight'].includes(eff),
99
- hasTranslateY: ['slideUp', 'slideDown', 'bounce'].includes(eff),
100
- hasRotateEffect: ['rotate', 'rotateScale', 'flip'].includes(eff),
101
- }), []);
102
203
  // Memoize effect type checks to avoid repeated includes() calls
103
- const effectFlags = useMemo(() => getEffectFlags(effect), [effect, getEffectFlags]);
204
+ const effectFlags = useMemo(() => getEffectFlags(effect), [effect]);
104
205
  // Exit effect flags (use exitEffect if specified, otherwise same as enter effect)
105
- const exitEffectFlags = useMemo(() => exitEffect ? getEffectFlags(exitEffect) : effectFlags, [exitEffect, getEffectFlags, effectFlags]);
206
+ const exitEffectFlags = useMemo(() => exitEffect ? getEffectFlags(exitEffect) : effectFlags, [exitEffect, effectFlags]);
106
207
  // Memoize interpolations to avoid recreating on every render
107
208
  const rotateInterpolation = useMemo(() => rotate.interpolate({
108
209
  inputRange: [-360, 0, 360],
@@ -135,51 +236,87 @@ intensity = 1, }) {
135
236
  // When skeleton is false, we animate height which doesn't support native driver
136
237
  // So we must use JS driver for all animations in that case
137
238
  const useNative = skeleton;
138
- const config = {
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 = {
139
248
  duration: currentDuration,
140
249
  easing: easingFn,
141
250
  useNativeDriver: useNative,
142
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
+ };
143
266
  const animations = [];
144
- // Opacity animation
267
+ // Opacity animation (always use timing for opacity for smoother fade)
145
268
  animations.push(Animated.timing(opacity, {
146
269
  toValue: toVisible ? 1 : 0,
147
- ...config,
270
+ ...timingConfig,
148
271
  }));
149
272
  // Scale animation
150
273
  if (currentFlags.hasScale) {
151
274
  const targetScale = toVisible ? 1 : getInitialScaleValue(currentEffect);
152
- animations.push(Animated.timing(scale, {
153
- toValue: targetScale,
154
- ...config,
155
- easing: currentEffect === 'bounce' ? Easing.bounce : easingFn,
156
- }));
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));
157
287
  }
158
288
  // Rotate animation
159
289
  if (currentFlags.hasRotate || currentFlags.hasFlip) {
160
290
  const targetRotate = toVisible ? 0 : getInitialRotateValue(currentEffect);
161
- animations.push(Animated.timing(rotate, {
162
- toValue: targetRotate,
163
- ...config,
164
- }));
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));
165
300
  }
166
301
  // TranslateX animation
167
302
  if (currentFlags.hasTranslateX) {
168
303
  const targetX = toVisible ? 0 : getInitialTranslateXValue(currentEffect);
169
- animations.push(Animated.timing(translateX, {
170
- toValue: targetX,
171
- ...config,
172
- }));
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));
173
313
  }
174
314
  // TranslateY animation
175
315
  if (currentFlags.hasTranslateY) {
176
316
  const targetY = toVisible ? 0 : getInitialTranslateYValue(currentEffect);
177
- animations.push(Animated.timing(translateY, {
178
- toValue: targetY,
179
- ...config,
180
- }));
317
+ animations.push(createAnimation(translateY, targetY));
181
318
  }
182
- // Height animation for non-skeleton mode
319
+ // Height animation for non-skeleton mode (always use timing)
183
320
  if (!skeleton && measuredHeight !== null) {
184
321
  const targetHeight = toVisible ? measuredHeight : 0;
185
322
  animations.push(Animated.timing(animatedHeight, {
@@ -295,6 +432,8 @@ intensity = 1, }) {
295
432
  animatedHeight,
296
433
  loop,
297
434
  loopDelay,
435
+ useSpring,
436
+ springConfig,
298
437
  ]);
299
438
  // Handle initial mount animation
300
439
  useEffect(() => {
@@ -320,11 +459,33 @@ intensity = 1, }) {
320
459
  }
321
460
  };
322
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]);
323
472
  // Memoize transform array to avoid recreating on every render
324
473
  // IMPORTANT: All hooks must be called before any conditional returns
325
474
  const transform = useMemo(() => {
326
475
  const { hasScale, hasRotate, hasFlip, hasTranslateX, hasTranslateY } = effectFlags;
476
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
327
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
328
489
  if (hasScale) {
329
490
  transforms.push({ scale });
330
491
  }
@@ -334,6 +495,16 @@ intensity = 1, }) {
334
495
  if (hasFlip) {
335
496
  transforms.push({ rotateY: flipInterpolation });
336
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
337
508
  if (hasTranslateX) {
338
509
  transforms.push({ translateX });
339
510
  }
@@ -341,7 +512,7 @@ intensity = 1, }) {
341
512
  transforms.push({ translateY });
342
513
  }
343
514
  return transforms.length > 0 ? transforms : undefined;
344
- }, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY]);
515
+ }, [effectFlags, scale, rotateInterpolation, flipInterpolation, translateX, translateY, anchorPoint, anchorOffset]);
345
516
  // Memoize animated style
346
517
  const animatedStyle = useMemo(() => ({
347
518
  opacity,
@@ -363,6 +534,8 @@ intensity = 1, }) {
363
534
  }
364
535
  return (_jsx(Animated.View, { style: [styles.container, style, containerAnimatedStyle], testID: testID, children: _jsx(Animated.View, { style: animatedStyle, children: children }) }));
365
536
  }
537
+ // Memoize PuffPop to prevent unnecessary re-renders
538
+ export const PuffPop = memo(PuffPopComponent, arePuffPopPropsEqual);
366
539
  /**
367
540
  * Get initial scale value based on effect
368
541
  */
@@ -371,6 +544,7 @@ function getInitialScale(effect, _reverse = false) {
371
544
  switch (effect) {
372
545
  case 'scale':
373
546
  case 'rotateScale':
547
+ case 'elastic':
374
548
  return 0;
375
549
  case 'bounce':
376
550
  return 0.3;
@@ -378,6 +552,8 @@ function getInitialScale(effect, _reverse = false) {
378
552
  return 0.5;
379
553
  case 'flip':
380
554
  return 0.8;
555
+ case 'pulse':
556
+ return 0.6;
381
557
  default:
382
558
  return 1;
383
559
  }
@@ -394,6 +570,10 @@ function getInitialRotate(effect, reverse = false) {
394
570
  return -180 * multiplier;
395
571
  case 'flip':
396
572
  return -180 * multiplier;
573
+ case 'swing':
574
+ return -15 * multiplier;
575
+ case 'wobble':
576
+ return -5 * multiplier;
397
577
  default:
398
578
  return 0;
399
579
  }
@@ -408,6 +588,10 @@ function getInitialTranslateX(effect, reverse = false) {
408
588
  return 100 * multiplier;
409
589
  case 'slideRight':
410
590
  return -100 * multiplier;
591
+ case 'shake':
592
+ return -10 * multiplier;
593
+ case 'wobble':
594
+ return -25 * multiplier;
411
595
  default:
412
596
  return 0;
413
597
  }
@@ -440,6 +624,68 @@ const styles = StyleSheet.create({
440
624
  },
441
625
  groupContainer: {},
442
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
+ }
443
689
  /**
444
690
  * PuffPopGroup - Animate multiple children with staggered entrance effects
445
691
  *
@@ -452,15 +698,19 @@ const styles = StyleSheet.create({
452
698
  * </PuffPopGroup>
453
699
  * ```
454
700
  */
455
- 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,
456
702
  // Custom initial values
457
703
  initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY,
458
704
  // Reverse mode
459
705
  reverse,
460
706
  // Animation intensity
461
707
  intensity,
708
+ // Anchor point
709
+ anchorPoint,
710
+ // Spring animation
711
+ useSpring, springConfig,
462
712
  // Exit animation settings
463
- exitEffect, exitDuration, exitEasing, exitDelay, }) {
713
+ exitEffect, exitDuration, exitEasing, exitDelay, exitStaggerDelay = 0, exitStaggerDirection = 'reverse', }) {
464
714
  const childArray = Children.toArray(children);
465
715
  const childCount = childArray.length;
466
716
  const completedCount = useRef(0);
@@ -489,6 +739,34 @@ exitEffect, exitDuration, exitEasing, exitDelay, }) {
489
739
  }
490
740
  return initialDelay + delayIndex * staggerDelay;
491
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]);
492
770
  // Handle individual child animation complete
493
771
  const handleChildComplete = useCallback(() => {
494
772
  completedCount.current += 1;
@@ -519,6 +797,8 @@ exitEffect, exitDuration, exitEasing, exitDelay, }) {
519
797
  }
520
798
  return baseStyle;
521
799
  }, [horizontal, gap]);
522
- 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, exitEffect: exitEffect, exitDuration: exitDuration, exitEasing: exitEasing, exitDelay: exitDelay, 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))) }));
523
801
  }
802
+ // Memoize PuffPopGroup to prevent unnecessary re-renders
803
+ export const PuffPopGroup = memo(PuffPopGroupComponent, arePuffPopGroupPropsEqual);
524
804
  export default PuffPop;
@@ -3,11 +3,42 @@ import { type StyleProp, type ViewStyle } from 'react-native';
3
3
  /**
4
4
  * Animation effect types for PuffPop
5
5
  */
6
- export type PuffPopEffect = 'scale' | 'rotate' | 'fade' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | 'bounce' | 'flip' | 'zoom' | 'rotateScale';
6
+ export type PuffPopEffect = 'scale' | 'rotate' | 'fade' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | 'bounce' | 'flip' | 'zoom' | 'rotateScale' | 'shake' | 'pulse' | 'swing' | 'wobble' | 'elastic';
7
7
  /**
8
8
  * Easing function types
9
9
  */
10
10
  export type PuffPopEasing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'spring' | 'bounce';
11
+ /**
12
+ * Anchor point for scale/rotate transformations
13
+ * Determines the origin point of the transformation
14
+ */
15
+ export type PuffPopAnchorPoint = 'center' | 'top' | 'bottom' | 'left' | 'right' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
16
+ /**
17
+ * Spring animation configuration
18
+ * Used when useSpring is true for physics-based animations
19
+ */
20
+ export interface PuffPopSpringConfig {
21
+ /**
22
+ * Controls the spring stiffness. Higher values = faster, snappier animation
23
+ * @default 100
24
+ */
25
+ tension?: number;
26
+ /**
27
+ * Controls the spring damping. Higher values = less oscillation
28
+ * @default 10
29
+ */
30
+ friction?: number;
31
+ /**
32
+ * Animation speed multiplier
33
+ * @default 12
34
+ */
35
+ speed?: number;
36
+ /**
37
+ * Controls the bounciness. Higher values = more bouncy
38
+ * @default 8
39
+ */
40
+ bounciness?: number;
41
+ }
11
42
  export interface PuffPopProps {
12
43
  /**
13
44
  * Children to animate
@@ -145,11 +176,32 @@ export interface PuffPopProps {
145
176
  * @default 1
146
177
  */
147
178
  intensity?: number;
179
+ /**
180
+ * Anchor point for scale/rotate transformations
181
+ * Determines the origin point of the transformation
182
+ * - 'center' = default center point
183
+ * - 'top', 'bottom', 'left', 'right' = edge centers
184
+ * - 'topLeft', 'topRight', 'bottomLeft', 'bottomRight' = corners
185
+ * @default 'center'
186
+ */
187
+ anchorPoint?: PuffPopAnchorPoint;
188
+ /**
189
+ * Use spring physics-based animation instead of timing
190
+ * Provides more natural, bouncy animations
191
+ * @default false
192
+ */
193
+ useSpring?: boolean;
194
+ /**
195
+ * Spring animation configuration
196
+ * Only used when useSpring is true
197
+ */
198
+ springConfig?: PuffPopSpringConfig;
148
199
  }
149
200
  /**
150
201
  * PuffPop - Animate children with beautiful entrance effects
151
202
  */
152
- export declare function PuffPop({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, onAnimationStart, style, animateOnMount, loop, loopDelay, respectReduceMotion, testID, exitEffect, exitDuration, exitEasing, exitDelay, initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY, reverse, intensity, }: PuffPopProps): ReactElement;
203
+ declare function PuffPopComponent({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, onAnimationStart, style, animateOnMount, loop, loopDelay, respectReduceMotion, testID, exitEffect, exitDuration, exitEasing, exitDelay, initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY, reverse, intensity, anchorPoint, useSpring, springConfig, }: PuffPopProps): ReactElement;
204
+ export declare const PuffPop: import("react").MemoExoticComponent<typeof PuffPopComponent>;
153
205
  /**
154
206
  * Props for PuffPopGroup component
155
207
  */
@@ -268,6 +320,20 @@ export interface PuffPopGroupProps {
268
320
  * @default 1
269
321
  */
270
322
  intensity?: number;
323
+ /**
324
+ * Anchor point for scale/rotate transformations for all children
325
+ * @default 'center'
326
+ */
327
+ anchorPoint?: PuffPopAnchorPoint;
328
+ /**
329
+ * Use spring physics-based animation for all children
330
+ * @default false
331
+ */
332
+ useSpring?: boolean;
333
+ /**
334
+ * Spring animation configuration for all children
335
+ */
336
+ springConfig?: PuffPopSpringConfig;
271
337
  /**
272
338
  * Animation effect type when hiding (exit animation) for all children
273
339
  * If not specified, uses the reverse of the enter effect
@@ -288,6 +354,21 @@ export interface PuffPopGroupProps {
288
354
  * @default 0
289
355
  */
290
356
  exitDelay?: number;
357
+ /**
358
+ * Delay between each child's exit animation start in milliseconds
359
+ * If not specified, all children exit simultaneously
360
+ * @default 0
361
+ */
362
+ exitStaggerDelay?: number;
363
+ /**
364
+ * Direction of exit stagger animation
365
+ * - 'forward': First to last child
366
+ * - 'reverse': Last to first child (most natural for exit)
367
+ * - 'center': From center outward
368
+ * - 'edges': From edges toward center
369
+ * @default 'reverse'
370
+ */
371
+ exitStaggerDirection?: 'forward' | 'reverse' | 'center' | 'edges';
291
372
  }
292
373
  /**
293
374
  * PuffPopGroup - Animate multiple children with staggered entrance effects
@@ -301,5 +382,6 @@ export interface PuffPopGroupProps {
301
382
  * </PuffPopGroup>
302
383
  * ```
303
384
  */
304
- export declare function PuffPopGroup({ children, effect, duration, staggerDelay, initialDelay, easing, skeleton, visible, animateOnMount, onAnimationComplete, onAnimationStart, style, respectReduceMotion, testID, staggerDirection, horizontal, gap, initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY, reverse, intensity, exitEffect, exitDuration, exitEasing, exitDelay, }: PuffPopGroupProps): ReactElement;
385
+ declare function PuffPopGroupComponent({ children, effect, duration, staggerDelay, initialDelay, easing, skeleton, visible, animateOnMount, onAnimationComplete, onAnimationStart, style, respectReduceMotion, testID, staggerDirection, horizontal, gap, initialOpacity, initialScale, initialRotate, initialTranslateX, initialTranslateY, reverse, intensity, anchorPoint, useSpring, springConfig, exitEffect, exitDuration, exitEasing, exitDelay, exitStaggerDelay, exitStaggerDirection, }: PuffPopGroupProps): ReactElement;
386
+ export declare const PuffPopGroup: import("react").MemoExoticComponent<typeof PuffPopGroupComponent>;
305
387
  export default PuffPop;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-puff-pop",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "A React Native animation library for revealing children components with beautiful puff and pop effects",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",