react-native-puff-pop 1.0.4 → 1.0.5

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/README.md CHANGED
@@ -102,6 +102,35 @@ By default, `skeleton={true}` reserves space for the component before animation:
102
102
 
103
103
  ### Staggered Animations
104
104
 
105
+ Use `PuffPopGroup` for easy staggered animations:
106
+
107
+ ```tsx
108
+ import { PuffPopGroup } from 'react-native-puff-pop';
109
+
110
+ // Simple stagger
111
+ <PuffPopGroup staggerDelay={100} effect="scale">
112
+ <Card title="First" />
113
+ <Card title="Second" />
114
+ <Card title="Third" />
115
+ </PuffPopGroup>
116
+
117
+ // With different directions
118
+ <PuffPopGroup staggerDelay={80} staggerDirection="reverse">
119
+ <Item />
120
+ <Item />
121
+ <Item />
122
+ </PuffPopGroup>
123
+
124
+ // Horizontal layout with gap
125
+ <PuffPopGroup horizontal gap={12} effect="slideUp">
126
+ <Avatar />
127
+ <Avatar />
128
+ <Avatar />
129
+ </PuffPopGroup>
130
+ ```
131
+
132
+ Or use manual delays with `PuffPop`:
133
+
105
134
  ```tsx
106
135
  <View>
107
136
  <PuffPop delay={0}>
@@ -172,6 +201,28 @@ function App() {
172
201
  | `respectReduceMotion` | `boolean` | `true` | Respect system reduce motion setting |
173
202
  | `testID` | `string` | - | Test ID for testing purposes |
174
203
 
204
+ ### PuffPopGroup Props
205
+
206
+ | Prop | Type | Default | Description |
207
+ |------|------|---------|-------------|
208
+ | `children` | `ReactNode` | - | Children to animate with stagger effect |
209
+ | `effect` | `PuffPopEffect` | `'scale'` | Animation effect for all children |
210
+ | `duration` | `number` | `400` | Animation duration for each child in ms |
211
+ | `staggerDelay` | `number` | `100` | Delay between each child's animation in ms |
212
+ | `initialDelay` | `number` | `0` | Delay before the first child animates in ms |
213
+ | `easing` | `PuffPopEasing` | `'easeOut'` | Easing function for all children |
214
+ | `skeleton` | `boolean` | `true` | Reserve space before animation |
215
+ | `visible` | `boolean` | `true` | Control visibility of all children |
216
+ | `animateOnMount` | `boolean` | `true` | Animate when component mounts |
217
+ | `onAnimationStart` | `() => void` | - | Callback when first child starts animating |
218
+ | `onAnimationComplete` | `() => void` | - | Callback when all children complete |
219
+ | `style` | `ViewStyle` | - | Custom container style |
220
+ | `staggerDirection` | `'forward' \| 'reverse' \| 'center' \| 'edges'` | `'forward'` | Direction of stagger animation |
221
+ | `horizontal` | `boolean` | `false` | Render children in horizontal layout |
222
+ | `gap` | `number` | - | Gap between children |
223
+ | `respectReduceMotion` | `boolean` | `true` | Respect system reduce motion setting |
224
+ | `testID` | `string` | - | Test ID for testing purposes |
225
+
175
226
  ### Animation Effects (`PuffPopEffect`)
176
227
 
177
228
  | Effect | Description |
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState, useCallback, useMemo, } from 'react';
2
+ import { useEffect, useRef, useState, useCallback, useMemo, Children, } from 'react';
3
3
  import { View, Animated, StyleSheet, Easing, AccessibilityInfo, } from 'react-native';
4
4
  /**
5
5
  * Get easing function based on type
@@ -173,11 +173,19 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
173
173
  // Stop any existing loop animation
174
174
  if (loopAnimationRef.current) {
175
175
  loopAnimationRef.current.stop();
176
+ loopAnimationRef.current = null;
177
+ }
178
+ // Clear any existing timeout
179
+ if (loopTimeoutRef.current) {
180
+ clearTimeout(loopTimeoutRef.current);
181
+ loopTimeoutRef.current = null;
176
182
  }
177
183
  const loopCount = typeof loop === 'number' ? loop : -1;
178
184
  let currentIteration = 0;
179
185
  const runLoop = () => {
180
186
  resetValues();
187
+ // Store the current animation reference so it can be stopped
188
+ loopAnimationRef.current = animation;
181
189
  animation.start(({ finished }) => {
182
190
  if (finished) {
183
191
  currentIteration++;
@@ -194,13 +202,17 @@ export function PuffPop({ children, effect = 'scale', duration = 400, delay = 0,
194
202
  runLoop();
195
203
  }
196
204
  }
197
- else if (onAnimationComplete) {
198
- onAnimationComplete();
205
+ else {
206
+ // Loop finished, clear reference
207
+ loopAnimationRef.current = null;
208
+ if (onAnimationComplete) {
209
+ onAnimationComplete();
210
+ }
199
211
  }
200
212
  }
201
213
  });
202
214
  };
203
- // Store reference and start
215
+ // Start the loop
204
216
  runLoop();
205
217
  }
206
218
  else {
@@ -372,5 +384,79 @@ const styles = StyleSheet.create({
372
384
  hidden: {
373
385
  opacity: 0,
374
386
  },
387
+ groupContainer: {},
375
388
  });
389
+ /**
390
+ * PuffPopGroup - Animate multiple children with staggered entrance effects
391
+ *
392
+ * @example
393
+ * ```tsx
394
+ * <PuffPopGroup staggerDelay={100} effect="scale">
395
+ * <Card title="First" />
396
+ * <Card title="Second" />
397
+ * <Card title="Third" />
398
+ * </PuffPopGroup>
399
+ * ```
400
+ */
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, }) {
402
+ const childArray = Children.toArray(children);
403
+ const childCount = childArray.length;
404
+ const completedCount = useRef(0);
405
+ const hasCalledStart = useRef(false);
406
+ // Calculate delay for each child based on stagger direction
407
+ const getChildDelay = useCallback((index) => {
408
+ let delayIndex;
409
+ switch (staggerDirection) {
410
+ case 'reverse':
411
+ delayIndex = childCount - 1 - index;
412
+ break;
413
+ case 'center': {
414
+ const center = (childCount - 1) / 2;
415
+ delayIndex = Math.abs(index - center);
416
+ break;
417
+ }
418
+ case 'edges': {
419
+ const center = (childCount - 1) / 2;
420
+ delayIndex = center - Math.abs(index - center);
421
+ break;
422
+ }
423
+ case 'forward':
424
+ default:
425
+ delayIndex = index;
426
+ break;
427
+ }
428
+ return initialDelay + delayIndex * staggerDelay;
429
+ }, [childCount, initialDelay, staggerDelay, staggerDirection]);
430
+ // Handle individual child animation complete
431
+ const handleChildComplete = useCallback(() => {
432
+ completedCount.current += 1;
433
+ if (completedCount.current >= childCount && onAnimationComplete) {
434
+ onAnimationComplete();
435
+ }
436
+ }, [childCount, onAnimationComplete]);
437
+ // Handle first child animation start
438
+ const handleChildStart = useCallback(() => {
439
+ if (!hasCalledStart.current && onAnimationStart) {
440
+ hasCalledStart.current = true;
441
+ onAnimationStart();
442
+ }
443
+ }, [onAnimationStart]);
444
+ // Reset counters when visibility changes
445
+ useEffect(() => {
446
+ if (visible) {
447
+ completedCount.current = 0;
448
+ hasCalledStart.current = false;
449
+ }
450
+ }, [visible]);
451
+ const containerStyle = useMemo(() => {
452
+ const baseStyle = {
453
+ flexDirection: horizontal ? 'row' : 'column',
454
+ };
455
+ if (gap !== undefined) {
456
+ baseStyle.gap = gap;
457
+ }
458
+ return baseStyle;
459
+ }, [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))) }));
461
+ }
376
462
  export default PuffPop;
@@ -88,4 +88,105 @@ export interface PuffPopProps {
88
88
  * PuffPop - Animate children with beautiful entrance effects
89
89
  */
90
90
  export declare function PuffPop({ children, effect, duration, delay, easing, skeleton, visible, onAnimationComplete, onAnimationStart, style, animateOnMount, loop, loopDelay, respectReduceMotion, testID, }: PuffPopProps): ReactElement;
91
+ /**
92
+ * Props for PuffPopGroup component
93
+ */
94
+ export interface PuffPopGroupProps {
95
+ /**
96
+ * Children to animate with stagger effect
97
+ */
98
+ children: ReactNode;
99
+ /**
100
+ * Animation effect type applied to all children
101
+ * @default 'scale'
102
+ */
103
+ effect?: PuffPopEffect;
104
+ /**
105
+ * Base animation duration in milliseconds for each child
106
+ * @default 400
107
+ */
108
+ duration?: number;
109
+ /**
110
+ * Delay between each child's animation start in milliseconds
111
+ * @default 100
112
+ */
113
+ staggerDelay?: number;
114
+ /**
115
+ * Initial delay before the first child animates in milliseconds
116
+ * @default 0
117
+ */
118
+ initialDelay?: number;
119
+ /**
120
+ * Easing function for all children
121
+ * @default 'easeOut'
122
+ */
123
+ easing?: PuffPopEasing;
124
+ /**
125
+ * If true, reserves space for children before animation
126
+ * @default true
127
+ */
128
+ skeleton?: boolean;
129
+ /**
130
+ * Whether children are visible
131
+ * @default true
132
+ */
133
+ visible?: boolean;
134
+ /**
135
+ * Whether to animate on mount
136
+ * @default true
137
+ */
138
+ animateOnMount?: boolean;
139
+ /**
140
+ * Callback when all children animations complete
141
+ */
142
+ onAnimationComplete?: () => void;
143
+ /**
144
+ * Callback when the first child animation starts
145
+ */
146
+ onAnimationStart?: () => void;
147
+ /**
148
+ * Custom style for the group container
149
+ */
150
+ style?: StyleProp<ViewStyle>;
151
+ /**
152
+ * Respect system reduce motion accessibility setting
153
+ * @default true
154
+ */
155
+ respectReduceMotion?: boolean;
156
+ /**
157
+ * Test ID for testing purposes
158
+ */
159
+ testID?: string;
160
+ /**
161
+ * Direction of stagger animation
162
+ * - 'forward': First to last child
163
+ * - 'reverse': Last to first child
164
+ * - 'center': From center outward
165
+ * - 'edges': From edges toward center
166
+ * @default 'forward'
167
+ */
168
+ staggerDirection?: 'forward' | 'reverse' | 'center' | 'edges';
169
+ /**
170
+ * If true, children are rendered in a row (horizontal layout)
171
+ * @default false
172
+ */
173
+ horizontal?: boolean;
174
+ /**
175
+ * Gap between children (uses flexbox gap)
176
+ */
177
+ gap?: number;
178
+ }
179
+ /**
180
+ * PuffPopGroup - Animate multiple children with staggered entrance effects
181
+ *
182
+ * @example
183
+ * ```tsx
184
+ * <PuffPopGroup staggerDelay={100} effect="scale">
185
+ * <Card title="First" />
186
+ * <Card title="Second" />
187
+ * <Card title="Third" />
188
+ * </PuffPopGroup>
189
+ * ```
190
+ */
191
+ export declare function PuffPopGroup({ children, effect, duration, staggerDelay, initialDelay, easing, skeleton, visible, animateOnMount, onAnimationComplete, onAnimationStart, style, respectReduceMotion, testID, staggerDirection, horizontal, gap, }: PuffPopGroupProps): ReactElement;
91
192
  export default PuffPop;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-puff-pop",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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",
package/src/index.tsx CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  useState,
5
5
  useCallback,
6
6
  useMemo,
7
+ Children,
7
8
  type ReactNode,
8
9
  type ReactElement,
9
10
  } from 'react';
@@ -370,6 +371,12 @@ export function PuffPop({
370
371
  // Stop any existing loop animation
371
372
  if (loopAnimationRef.current) {
372
373
  loopAnimationRef.current.stop();
374
+ loopAnimationRef.current = null;
375
+ }
376
+ // Clear any existing timeout
377
+ if (loopTimeoutRef.current) {
378
+ clearTimeout(loopTimeoutRef.current);
379
+ loopTimeoutRef.current = null;
373
380
  }
374
381
 
375
382
  const loopCount = typeof loop === 'number' ? loop : -1;
@@ -377,6 +384,8 @@ export function PuffPop({
377
384
 
378
385
  const runLoop = () => {
379
386
  resetValues();
387
+ // Store the current animation reference so it can be stopped
388
+ loopAnimationRef.current = animation;
380
389
  animation.start(({ finished }) => {
381
390
  if (finished) {
382
391
  currentIteration++;
@@ -391,14 +400,18 @@ export function PuffPop({
391
400
  } else {
392
401
  runLoop();
393
402
  }
394
- } else if (onAnimationComplete) {
395
- onAnimationComplete();
403
+ } else {
404
+ // Loop finished, clear reference
405
+ loopAnimationRef.current = null;
406
+ if (onAnimationComplete) {
407
+ onAnimationComplete();
408
+ }
396
409
  }
397
410
  }
398
411
  });
399
412
  };
400
413
 
401
- // Store reference and start
414
+ // Start the loop
402
415
  runLoop();
403
416
  } else {
404
417
  // Stop any existing loop animation
@@ -602,7 +615,236 @@ const styles = StyleSheet.create({
602
615
  hidden: {
603
616
  opacity: 0,
604
617
  },
618
+ groupContainer: {},
605
619
  });
606
620
 
621
+ /**
622
+ * Props for PuffPopGroup component
623
+ */
624
+ export interface PuffPopGroupProps {
625
+ /**
626
+ * Children to animate with stagger effect
627
+ */
628
+ children: ReactNode;
629
+
630
+ /**
631
+ * Animation effect type applied to all children
632
+ * @default 'scale'
633
+ */
634
+ effect?: PuffPopEffect;
635
+
636
+ /**
637
+ * Base animation duration in milliseconds for each child
638
+ * @default 400
639
+ */
640
+ duration?: number;
641
+
642
+ /**
643
+ * Delay between each child's animation start in milliseconds
644
+ * @default 100
645
+ */
646
+ staggerDelay?: number;
647
+
648
+ /**
649
+ * Initial delay before the first child animates in milliseconds
650
+ * @default 0
651
+ */
652
+ initialDelay?: number;
653
+
654
+ /**
655
+ * Easing function for all children
656
+ * @default 'easeOut'
657
+ */
658
+ easing?: PuffPopEasing;
659
+
660
+ /**
661
+ * If true, reserves space for children before animation
662
+ * @default true
663
+ */
664
+ skeleton?: boolean;
665
+
666
+ /**
667
+ * Whether children are visible
668
+ * @default true
669
+ */
670
+ visible?: boolean;
671
+
672
+ /**
673
+ * Whether to animate on mount
674
+ * @default true
675
+ */
676
+ animateOnMount?: boolean;
677
+
678
+ /**
679
+ * Callback when all children animations complete
680
+ */
681
+ onAnimationComplete?: () => void;
682
+
683
+ /**
684
+ * Callback when the first child animation starts
685
+ */
686
+ onAnimationStart?: () => void;
687
+
688
+ /**
689
+ * Custom style for the group container
690
+ */
691
+ style?: StyleProp<ViewStyle>;
692
+
693
+ /**
694
+ * Respect system reduce motion accessibility setting
695
+ * @default true
696
+ */
697
+ respectReduceMotion?: boolean;
698
+
699
+ /**
700
+ * Test ID for testing purposes
701
+ */
702
+ testID?: string;
703
+
704
+ /**
705
+ * Direction of stagger animation
706
+ * - 'forward': First to last child
707
+ * - 'reverse': Last to first child
708
+ * - 'center': From center outward
709
+ * - 'edges': From edges toward center
710
+ * @default 'forward'
711
+ */
712
+ staggerDirection?: 'forward' | 'reverse' | 'center' | 'edges';
713
+
714
+ /**
715
+ * If true, children are rendered in a row (horizontal layout)
716
+ * @default false
717
+ */
718
+ horizontal?: boolean;
719
+
720
+ /**
721
+ * Gap between children (uses flexbox gap)
722
+ */
723
+ gap?: number;
724
+ }
725
+
726
+ /**
727
+ * PuffPopGroup - Animate multiple children with staggered entrance effects
728
+ *
729
+ * @example
730
+ * ```tsx
731
+ * <PuffPopGroup staggerDelay={100} effect="scale">
732
+ * <Card title="First" />
733
+ * <Card title="Second" />
734
+ * <Card title="Third" />
735
+ * </PuffPopGroup>
736
+ * ```
737
+ */
738
+ export function PuffPopGroup({
739
+ children,
740
+ effect = 'scale',
741
+ duration = 400,
742
+ staggerDelay = 100,
743
+ initialDelay = 0,
744
+ easing = 'easeOut',
745
+ skeleton = true,
746
+ visible = true,
747
+ animateOnMount = true,
748
+ onAnimationComplete,
749
+ onAnimationStart,
750
+ style,
751
+ respectReduceMotion = true,
752
+ testID,
753
+ staggerDirection = 'forward',
754
+ horizontal = false,
755
+ gap,
756
+ }: PuffPopGroupProps): ReactElement {
757
+ const childArray = Children.toArray(children);
758
+ const childCount = childArray.length;
759
+ const completedCount = useRef(0);
760
+ const hasCalledStart = useRef(false);
761
+
762
+ // Calculate delay for each child based on stagger direction
763
+ const getChildDelay = useCallback(
764
+ (index: number): number => {
765
+ let delayIndex: number;
766
+
767
+ switch (staggerDirection) {
768
+ case 'reverse':
769
+ delayIndex = childCount - 1 - index;
770
+ break;
771
+ case 'center': {
772
+ const center = (childCount - 1) / 2;
773
+ delayIndex = Math.abs(index - center);
774
+ break;
775
+ }
776
+ case 'edges': {
777
+ const center = (childCount - 1) / 2;
778
+ delayIndex = center - Math.abs(index - center);
779
+ break;
780
+ }
781
+ case 'forward':
782
+ default:
783
+ delayIndex = index;
784
+ break;
785
+ }
786
+
787
+ return initialDelay + delayIndex * staggerDelay;
788
+ },
789
+ [childCount, initialDelay, staggerDelay, staggerDirection]
790
+ );
791
+
792
+ // Handle individual child animation complete
793
+ const handleChildComplete = useCallback(() => {
794
+ completedCount.current += 1;
795
+ if (completedCount.current >= childCount && onAnimationComplete) {
796
+ onAnimationComplete();
797
+ }
798
+ }, [childCount, onAnimationComplete]);
799
+
800
+ // Handle first child animation start
801
+ const handleChildStart = useCallback(() => {
802
+ if (!hasCalledStart.current && onAnimationStart) {
803
+ hasCalledStart.current = true;
804
+ onAnimationStart();
805
+ }
806
+ }, [onAnimationStart]);
807
+
808
+ // Reset counters when visibility changes
809
+ useEffect(() => {
810
+ if (visible) {
811
+ completedCount.current = 0;
812
+ hasCalledStart.current = false;
813
+ }
814
+ }, [visible]);
815
+
816
+ const containerStyle = useMemo(() => {
817
+ const baseStyle: ViewStyle = {
818
+ flexDirection: horizontal ? 'row' : 'column',
819
+ };
820
+ if (gap !== undefined) {
821
+ baseStyle.gap = gap;
822
+ }
823
+ return baseStyle;
824
+ }, [horizontal, gap]);
825
+
826
+ return (
827
+ <View style={[styles.groupContainer, containerStyle, style]} testID={testID}>
828
+ {childArray.map((child, index) => (
829
+ <PuffPop
830
+ key={index}
831
+ effect={effect}
832
+ duration={duration}
833
+ delay={getChildDelay(index)}
834
+ easing={easing}
835
+ skeleton={skeleton}
836
+ visible={visible}
837
+ animateOnMount={animateOnMount}
838
+ onAnimationComplete={handleChildComplete}
839
+ onAnimationStart={index === 0 ? handleChildStart : undefined}
840
+ respectReduceMotion={respectReduceMotion}
841
+ >
842
+ {child}
843
+ </PuffPop>
844
+ ))}
845
+ </View>
846
+ );
847
+ }
848
+
607
849
  export default PuffPop;
608
850