react-native-puff-pop 1.0.4 → 1.0.6
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 +51 -0
- package/lib/module/index.js +90 -4
- package/lib/typescript/src/index.d.ts +101 -0
- package/package.json +12 -1
- package/src/index.tsx +245 -3
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 |
|
package/lib/module/index.js
CHANGED
|
@@ -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
|
|
198
|
-
|
|
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
|
-
//
|
|
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.
|
|
3
|
+
"version": "1.0.6",
|
|
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",
|
|
@@ -60,10 +60,21 @@
|
|
|
60
60
|
"registry": "https://registry.npmjs.org/"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
+
"@babel/core": "^7.28.5",
|
|
64
|
+
"@babel/preset-env": "^7.28.5",
|
|
65
|
+
"@babel/preset-react": "^7.28.5",
|
|
66
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
67
|
+
"@testing-library/react-native": "^13.3.3",
|
|
68
|
+
"@types/babel__core": "^7",
|
|
69
|
+
"@types/babel__preset-env": "^7",
|
|
70
|
+
"@types/jest": "^30.0.0",
|
|
63
71
|
"@types/react": "^19.1.12",
|
|
72
|
+
"@types/react-test-renderer": "^19.1.0",
|
|
64
73
|
"del-cli": "^6.0.0",
|
|
74
|
+
"jest": "^30.2.0",
|
|
65
75
|
"react": "19.1.0",
|
|
66
76
|
"react-native": "0.81.5",
|
|
77
|
+
"react-test-renderer": "19.1.0",
|
|
67
78
|
"typescript": "^5.9.2"
|
|
68
79
|
},
|
|
69
80
|
"peerDependencies": {
|
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
|
|
395
|
-
|
|
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
|
-
//
|
|
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
|
|