react-native-ultra-carousel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +81 -0
  2. package/src/animations/basic/fade.ts +51 -0
  3. package/src/animations/basic/overlap.ts +69 -0
  4. package/src/animations/basic/parallax.ts +65 -0
  5. package/src/animations/basic/peek.ts +79 -0
  6. package/src/animations/basic/scale.ts +63 -0
  7. package/src/animations/basic/scaleFade.ts +73 -0
  8. package/src/animations/basic/slide.ts +53 -0
  9. package/src/animations/basic/slideFade.ts +60 -0
  10. package/src/animations/basic/vertical.ts +50 -0
  11. package/src/animations/basic/verticalFade.ts +60 -0
  12. package/src/animations/index.ts +45 -0
  13. package/src/animations/registry.ts +175 -0
  14. package/src/animations/types.ts +11 -0
  15. package/src/animations/utils.ts +75 -0
  16. package/src/components/AutoPlayController.tsx +38 -0
  17. package/src/components/Carousel.tsx +371 -0
  18. package/src/components/CarouselItem.tsx +98 -0
  19. package/src/components/Pagination/BarPagination.tsx +141 -0
  20. package/src/components/Pagination/CustomPagination.tsx +48 -0
  21. package/src/components/Pagination/DotPagination.tsx +137 -0
  22. package/src/components/Pagination/NumberPagination.tsx +117 -0
  23. package/src/components/Pagination/Pagination.tsx +82 -0
  24. package/src/components/Pagination/ProgressPagination.tsx +70 -0
  25. package/src/components/Pagination/index.ts +11 -0
  26. package/src/components/ParallaxImage.tsx +89 -0
  27. package/src/gestures/FlingGestureManager.ts +49 -0
  28. package/src/gestures/PanGestureManager.ts +202 -0
  29. package/src/gestures/ScrollViewCompat.ts +28 -0
  30. package/src/gestures/types.ts +6 -0
  31. package/src/hooks/useAnimationProgress.ts +33 -0
  32. package/src/hooks/useAutoPlay.ts +115 -0
  33. package/src/hooks/useCarousel.ts +118 -0
  34. package/src/hooks/useCarouselGesture.ts +109 -0
  35. package/src/hooks/useItemAnimation.ts +44 -0
  36. package/src/hooks/usePagination.ts +39 -0
  37. package/src/hooks/useSnapPoints.ts +31 -0
  38. package/src/hooks/useVirtualization.ts +63 -0
  39. package/src/index.ts +71 -0
  40. package/src/plugins/PluginManager.ts +150 -0
  41. package/src/plugins/types.ts +6 -0
  42. package/src/types/animation.ts +72 -0
  43. package/src/types/carousel.ts +188 -0
  44. package/src/types/gesture.ts +42 -0
  45. package/src/types/index.ts +41 -0
  46. package/src/types/pagination.ts +65 -0
  47. package/src/types/plugin.ts +27 -0
  48. package/src/utils/accessibility.ts +71 -0
  49. package/src/utils/constants.ts +45 -0
  50. package/src/utils/layout.ts +115 -0
  51. package/src/utils/math.ts +78 -0
  52. package/src/utils/platform.ts +33 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @file Pan gesture manager
3
+ * @description Handles pan gesture configuration and snap calculations for the carousel
4
+ */
5
+
6
+ import { Gesture } from 'react-native-gesture-handler';
7
+ import {
8
+ useSharedValue,
9
+ withSpring,
10
+ runOnJS,
11
+ } from 'react-native-reanimated';
12
+ import { useCallback, useMemo } from 'react';
13
+ import { DEFAULT_SPRING_CONFIG, DEFAULT_VELOCITY_THRESHOLD } from '../utils/constants';
14
+ import { clamp } from '../utils/math';
15
+ import { findNearestSnapIndex } from '../utils/layout';
16
+
17
+ /** Configuration for the pan gesture hook */
18
+ export interface PanGestureOptions {
19
+ /** Total number of items */
20
+ totalItems: number;
21
+ /** Size of each item (width or height depending on direction) */
22
+ itemSize: number;
23
+ /** Gap between items */
24
+ gap: number;
25
+ /** All snap point offsets */
26
+ snapPoints: number[];
27
+ /** Whether carousel is horizontal */
28
+ isHorizontal: boolean;
29
+ /** Whether loop mode is enabled */
30
+ loop: boolean;
31
+ /** Whether gesture is enabled */
32
+ enabled: boolean;
33
+ /** Active offset X threshold */
34
+ activeOffsetX: [number, number];
35
+ /** Active offset Y threshold */
36
+ activeOffsetY: [number, number];
37
+ /** Velocity threshold for fling */
38
+ velocityThreshold: number;
39
+ /** Callback when index changes */
40
+ onIndexChange?: (index: number) => void;
41
+ /** Callback when scroll starts */
42
+ onScrollStart?: () => void;
43
+ /** Callback when scroll ends */
44
+ onScrollEnd?: (index: number) => void;
45
+ /** Custom gesture configuration callback */
46
+ onConfigurePanGesture?: (gesture: ReturnType<typeof Gesture.Pan>) => void;
47
+ }
48
+
49
+ /**
50
+ * Creates and manages the pan gesture for carousel navigation.
51
+ *
52
+ * @param options - Configuration for the gesture
53
+ * @returns Object containing the gesture, shared values, and control methods
54
+ */
55
+ export const usePanGesture = (options: PanGestureOptions) => {
56
+ const {
57
+ totalItems,
58
+ itemSize,
59
+ gap,
60
+ snapPoints,
61
+ isHorizontal,
62
+ loop,
63
+ enabled,
64
+ activeOffsetX,
65
+ activeOffsetY,
66
+ velocityThreshold,
67
+ onIndexChange,
68
+ onScrollStart,
69
+ onScrollEnd,
70
+ onConfigurePanGesture,
71
+ } = options;
72
+
73
+ const offset = useSharedValue(0);
74
+ const activeIndex = useSharedValue(0);
75
+ const isGestureActive = useSharedValue(false);
76
+
77
+ const stepSize = itemSize + gap;
78
+ const maxOffset = (totalItems - 1) * stepSize;
79
+
80
+ const handleIndexChange = useCallback(
81
+ (index: number) => {
82
+ onIndexChange?.(index);
83
+ },
84
+ [onIndexChange]
85
+ );
86
+
87
+ const handleScrollStart = useCallback(() => {
88
+ onScrollStart?.();
89
+ }, [onScrollStart]);
90
+
91
+ const handleScrollEnd = useCallback(
92
+ (index: number) => {
93
+ onScrollEnd?.(index);
94
+ },
95
+ [onScrollEnd]
96
+ );
97
+
98
+ const snapToIndex = useCallback(
99
+ (index: number, animated = true) => {
100
+ const targetIndex = loop
101
+ ? index
102
+ : clamp(index, 0, totalItems - 1);
103
+ const targetOffset = snapPoints[targetIndex] ?? targetIndex * stepSize;
104
+
105
+ if (animated) {
106
+ offset.value = withSpring(targetOffset, DEFAULT_SPRING_CONFIG);
107
+ } else {
108
+ offset.value = targetOffset;
109
+ }
110
+
111
+ if (activeIndex.value !== targetIndex) {
112
+ activeIndex.value = targetIndex;
113
+ runOnJS(handleIndexChange)(targetIndex);
114
+ }
115
+ },
116
+ [loop, totalItems, snapPoints, stepSize, offset, activeIndex, handleIndexChange]
117
+ );
118
+
119
+ const gesture = useMemo(() => {
120
+ const pan = Gesture.Pan()
121
+ .activeOffsetX(activeOffsetX)
122
+ .activeOffsetY(activeOffsetY)
123
+ .enabled(enabled)
124
+ .onStart(() => {
125
+ isGestureActive.value = true;
126
+ runOnJS(handleScrollStart)();
127
+ })
128
+ .onUpdate((event) => {
129
+ const translation = isHorizontal
130
+ ? event.translationX
131
+ : event.translationY;
132
+ const startOffset = activeIndex.value * stepSize;
133
+ let newOffset = startOffset - translation;
134
+
135
+ if (!loop) {
136
+ newOffset = clamp(newOffset, 0, maxOffset);
137
+ }
138
+
139
+ offset.value = newOffset;
140
+ })
141
+ .onEnd((event) => {
142
+ isGestureActive.value = false;
143
+
144
+ const velocity = isHorizontal ? event.velocityX : event.velocityY;
145
+ const isFling = Math.abs(velocity) > velocityThreshold;
146
+
147
+ let targetIndex: number;
148
+
149
+ if (isFling) {
150
+ targetIndex = velocity < 0
151
+ ? activeIndex.value + 1
152
+ : activeIndex.value - 1;
153
+ } else {
154
+ targetIndex = findNearestSnapIndex(offset.value, snapPoints);
155
+ }
156
+
157
+ if (!loop) {
158
+ targetIndex = clamp(targetIndex, 0, totalItems - 1);
159
+ }
160
+
161
+ const targetOffset = snapPoints[targetIndex] ?? targetIndex * stepSize;
162
+
163
+ offset.value = withSpring(targetOffset, DEFAULT_SPRING_CONFIG);
164
+
165
+ if (activeIndex.value !== targetIndex) {
166
+ activeIndex.value = targetIndex;
167
+ runOnJS(handleIndexChange)(targetIndex);
168
+ }
169
+ runOnJS(handleScrollEnd)(targetIndex);
170
+ });
171
+
172
+ onConfigurePanGesture?.(pan);
173
+
174
+ return pan;
175
+ }, [
176
+ activeOffsetX,
177
+ activeOffsetY,
178
+ enabled,
179
+ isHorizontal,
180
+ loop,
181
+ maxOffset,
182
+ stepSize,
183
+ totalItems,
184
+ snapPoints,
185
+ velocityThreshold,
186
+ offset,
187
+ activeIndex,
188
+ isGestureActive,
189
+ handleIndexChange,
190
+ handleScrollStart,
191
+ handleScrollEnd,
192
+ onConfigurePanGesture,
193
+ ]);
194
+
195
+ return {
196
+ gesture,
197
+ offset,
198
+ activeIndex,
199
+ isGestureActive,
200
+ snapToIndex,
201
+ };
202
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @file ScrollView compatibility manager
3
+ * @description Resolves gesture conflicts between carousel and parent ScrollView
4
+ */
5
+
6
+ import { Gesture } from 'react-native-gesture-handler';
7
+
8
+ /**
9
+ * Configures a pan gesture for compatibility with a parent ScrollView.
10
+ * Sets appropriate active offsets so horizontal swipes activate the carousel
11
+ * while vertical swipes pass through to the ScrollView.
12
+ *
13
+ * @param panGesture - The pan gesture to configure
14
+ * @param isHorizontal - Whether the carousel scrolls horizontally
15
+ * @returns The configured gesture
16
+ */
17
+ export const configureScrollViewCompat = (
18
+ panGesture: ReturnType<typeof Gesture.Pan>,
19
+ isHorizontal: boolean
20
+ ) => {
21
+ if (isHorizontal) {
22
+ panGesture.activeOffsetX([-10, 10]).activeOffsetY([-50, 50]);
23
+ } else {
24
+ panGesture.activeOffsetY([-10, 10]).activeOffsetX([-50, 50]);
25
+ }
26
+
27
+ return panGesture;
28
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @file Gesture module types
3
+ * @description Internal types for gesture handling
4
+ */
5
+
6
+ export type { GestureState, PanGestureConfig, SnapResult } from '../types/gesture';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @file useAnimationProgress hook
3
+ * @description Computes normalized animation progress for each carousel item
4
+ */
5
+
6
+ import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
7
+
8
+ /**
9
+ * Computes the normalized animation progress for a specific item.
10
+ * Progress of 0 means the item is fully active/centered.
11
+ * Progress of -1 means the item is one position to the left (prev).
12
+ * Progress of +1 means the item is one position to the right (next).
13
+ *
14
+ * @param index - The item index
15
+ * @param scrollOffset - The current scroll offset shared value
16
+ * @param itemSize - Width or height of each item
17
+ * @param gap - Gap between items
18
+ * @returns Shared value containing the normalized progress
19
+ */
20
+ export const useAnimationProgress = (
21
+ index: number,
22
+ scrollOffset: SharedValue<number>,
23
+ itemSize: number,
24
+ gap: number
25
+ ): SharedValue<number> => {
26
+ const stepSize = itemSize + gap;
27
+
28
+ return useDerivedValue(() => {
29
+ if (stepSize === 0) return 0;
30
+ const itemOffset = index * stepSize;
31
+ return (scrollOffset.value - itemOffset) / stepSize;
32
+ }, [index, stepSize]);
33
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @file useAutoPlay hook
3
+ * @description Manages automatic slide advancement with pause/resume controls
4
+ */
5
+
6
+ import { useCallback, useEffect, useRef, useState } from 'react';
7
+ import type { AutoPlayConfig } from '../types';
8
+ import { DEFAULT_AUTO_PLAY_INTERVAL } from '../utils/constants';
9
+
10
+ /** Return type of the useAutoPlay hook */
11
+ export interface UseAutoPlayReturn {
12
+ /** Whether auto play is currently running */
13
+ isPlaying: boolean;
14
+ /** Start auto play */
15
+ start: () => void;
16
+ /** Stop auto play permanently until start is called */
17
+ stop: () => void;
18
+ /** Pause temporarily (e.g., during user interaction) */
19
+ pause: () => void;
20
+ /** Resume after a pause */
21
+ resume: () => void;
22
+ /** Notify the hook that user interacted (resets pause timer) */
23
+ onInteraction: () => void;
24
+ }
25
+
26
+ /**
27
+ * Manages automatic carousel advancement.
28
+ *
29
+ * @param config - Auto play configuration
30
+ * @param onAdvance - Callback to advance to the next or previous item
31
+ * @returns Auto play control methods and state
32
+ */
33
+ export const useAutoPlay = (
34
+ config: AutoPlayConfig | boolean | undefined,
35
+ onAdvance: (direction: 'forward' | 'backward') => void
36
+ ): UseAutoPlayReturn => {
37
+ const normalizedConfig: AutoPlayConfig = typeof config === 'boolean'
38
+ ? { enabled: config }
39
+ : config ?? { enabled: false };
40
+
41
+ const {
42
+ enabled,
43
+ interval = DEFAULT_AUTO_PLAY_INTERVAL,
44
+ pauseOnInteraction = true,
45
+ direction = 'forward',
46
+ } = normalizedConfig;
47
+
48
+ const [isPlaying, setIsPlaying] = useState(enabled);
49
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
50
+ const isPausedRef = useRef(false);
51
+
52
+ const clearTimer = useCallback(() => {
53
+ if (intervalRef.current !== null) {
54
+ clearInterval(intervalRef.current);
55
+ intervalRef.current = null;
56
+ }
57
+ }, []);
58
+
59
+ const startTimer = useCallback(() => {
60
+ clearTimer();
61
+ intervalRef.current = setInterval(() => {
62
+ if (!isPausedRef.current) {
63
+ onAdvance(direction);
64
+ }
65
+ }, interval);
66
+ }, [clearTimer, interval, direction, onAdvance]);
67
+
68
+ const start = useCallback(() => {
69
+ setIsPlaying(true);
70
+ isPausedRef.current = false;
71
+ startTimer();
72
+ }, [startTimer]);
73
+
74
+ const stop = useCallback(() => {
75
+ setIsPlaying(false);
76
+ isPausedRef.current = false;
77
+ clearTimer();
78
+ }, [clearTimer]);
79
+
80
+ const pause = useCallback(() => {
81
+ isPausedRef.current = true;
82
+ }, []);
83
+
84
+ const resume = useCallback(() => {
85
+ isPausedRef.current = false;
86
+ }, []);
87
+
88
+ const onInteraction = useCallback(() => {
89
+ if (pauseOnInteraction && isPlaying) {
90
+ pause();
91
+ setTimeout(() => {
92
+ resume();
93
+ }, interval);
94
+ }
95
+ }, [pauseOnInteraction, isPlaying, pause, resume, interval]);
96
+
97
+ useEffect(() => {
98
+ if (enabled) {
99
+ start();
100
+ } else {
101
+ stop();
102
+ }
103
+
104
+ return clearTimer;
105
+ }, [enabled, start, stop, clearTimer]);
106
+
107
+ return {
108
+ isPlaying,
109
+ start,
110
+ stop,
111
+ pause,
112
+ resume,
113
+ onInteraction,
114
+ };
115
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @file useCarousel hook
3
+ * @description Main hook for external carousel control and state management
4
+ */
5
+
6
+ import { useCallback, useRef, useState } from 'react';
7
+ import type { CarouselRef } from '../types';
8
+
9
+ /** Return type of the useCarousel hook */
10
+ export interface UseCarouselReturn {
11
+ /** Ref to attach to the Carousel component */
12
+ ref: React.RefObject<CarouselRef>;
13
+ /** Current active index (reactive) */
14
+ activeIndex: number;
15
+ /** Total number of items (set when carousel mounts) */
16
+ totalItems: number;
17
+ /** Scroll to a specific index */
18
+ scrollTo: (index: number, animated?: boolean) => void;
19
+ /** Go to next item */
20
+ next: (animated?: boolean) => void;
21
+ /** Go to previous item */
22
+ prev: (animated?: boolean) => void;
23
+ /** Whether carousel is currently animating */
24
+ isAnimating: boolean;
25
+ /** Auto play controls */
26
+ autoPlay: {
27
+ start: () => void;
28
+ stop: () => void;
29
+ pause: () => void;
30
+ isPlaying: boolean;
31
+ };
32
+ /** Internal: callback for carousel to report index changes */
33
+ _onIndexChange: (index: number) => void;
34
+ /** Internal: callback for carousel to report total items */
35
+ _setTotalItems: (count: number) => void;
36
+ }
37
+
38
+ /**
39
+ * External hook for controlling and observing a Carousel component.
40
+ * Attach the returned ref to a Carousel component for full control.
41
+ *
42
+ * @returns Carousel control interface
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * const carousel = useCarousel();
47
+ *
48
+ * <Carousel ref={carousel.ref} data={data} renderItem={renderItem} />
49
+ * <Button onPress={carousel.next} title="Next" />
50
+ * <Text>{carousel.activeIndex + 1} / {carousel.totalItems}</Text>
51
+ * ```
52
+ */
53
+ export const useCarousel = (): UseCarouselReturn => {
54
+ const ref = useRef<CarouselRef>(null);
55
+ const [activeIndex, setActiveIndex] = useState(0);
56
+ const [totalItems, setTotalItems] = useState(0);
57
+ const [isAnimating, setIsAnimating] = useState(false);
58
+ const [isPlaying, setIsPlaying] = useState(false);
59
+
60
+ const scrollTo = useCallback((index: number, animated = true) => {
61
+ setIsAnimating(true);
62
+ ref.current?.scrollTo(index, animated);
63
+ setTimeout(() => setIsAnimating(false), 300);
64
+ }, []);
65
+
66
+ const next = useCallback((animated = true) => {
67
+ setIsAnimating(true);
68
+ ref.current?.next(animated);
69
+ setTimeout(() => setIsAnimating(false), 300);
70
+ }, []);
71
+
72
+ const prev = useCallback((animated = true) => {
73
+ setIsAnimating(true);
74
+ ref.current?.prev(animated);
75
+ setTimeout(() => setIsAnimating(false), 300);
76
+ }, []);
77
+
78
+ const startAutoPlay = useCallback(() => {
79
+ ref.current?.startAutoPlay();
80
+ setIsPlaying(true);
81
+ }, []);
82
+
83
+ const stopAutoPlay = useCallback(() => {
84
+ ref.current?.stopAutoPlay();
85
+ setIsPlaying(false);
86
+ }, []);
87
+
88
+ const pauseAutoPlay = useCallback(() => {
89
+ ref.current?.pauseAutoPlay();
90
+ setIsPlaying(false);
91
+ }, []);
92
+
93
+ const _onIndexChange = useCallback((index: number) => {
94
+ setActiveIndex(index);
95
+ }, []);
96
+
97
+ const _setTotalItems = useCallback((count: number) => {
98
+ setTotalItems(count);
99
+ }, []);
100
+
101
+ return {
102
+ ref,
103
+ activeIndex,
104
+ totalItems,
105
+ scrollTo,
106
+ next,
107
+ prev,
108
+ isAnimating,
109
+ autoPlay: {
110
+ start: startAutoPlay,
111
+ stop: stopAutoPlay,
112
+ pause: pauseAutoPlay,
113
+ isPlaying,
114
+ },
115
+ _onIndexChange,
116
+ _setTotalItems,
117
+ };
118
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @file useCarouselGesture hook
3
+ * @description Orchestrates gesture handling for the carousel component
4
+ */
5
+
6
+ import { useMemo } from 'react';
7
+ import type { GestureConfig, CarouselDirection } from '../types';
8
+ import { usePanGesture, type PanGestureOptions } from '../gestures/PanGestureManager';
9
+ import {
10
+ DEFAULT_ACTIVE_OFFSET_X,
11
+ DEFAULT_ACTIVE_OFFSET_Y,
12
+ DEFAULT_VELOCITY_THRESHOLD,
13
+ } from '../utils/constants';
14
+
15
+ /** Options for the carousel gesture hook */
16
+ export interface UseCarouselGestureOptions {
17
+ /** Total items in the carousel */
18
+ totalItems: number;
19
+ /** Size of each item */
20
+ itemSize: number;
21
+ /** Gap between items */
22
+ gap: number;
23
+ /** Snap points array */
24
+ snapPoints: number[];
25
+ /** Scroll direction */
26
+ direction: CarouselDirection;
27
+ /** Whether loop is enabled */
28
+ loop: boolean;
29
+ /** Whether interactions are enabled */
30
+ enabled: boolean;
31
+ /** Custom gesture config */
32
+ gestureConfig?: GestureConfig;
33
+ /** Index change callback */
34
+ onIndexChange?: (index: number) => void;
35
+ /** Scroll start callback */
36
+ onScrollStart?: () => void;
37
+ /** Scroll end callback */
38
+ onScrollEnd?: (index: number) => void;
39
+ }
40
+
41
+ /**
42
+ * Sets up gesture handling for the carousel.
43
+ * Bridges user-facing gesture config with the internal PanGestureManager.
44
+ *
45
+ * @param options - Gesture configuration options
46
+ * @returns Gesture, shared values, and snap control
47
+ */
48
+ export const useCarouselGesture = (options: UseCarouselGestureOptions) => {
49
+ const {
50
+ totalItems,
51
+ itemSize,
52
+ gap,
53
+ snapPoints,
54
+ direction,
55
+ loop,
56
+ enabled,
57
+ gestureConfig,
58
+ onIndexChange,
59
+ onScrollStart,
60
+ onScrollEnd,
61
+ } = options;
62
+
63
+ const isHorizontal = direction === 'horizontal';
64
+
65
+ const panOptions: PanGestureOptions = useMemo(() => {
66
+ const activeOffsetX = gestureConfig?.activeOffsetX
67
+ ? (Array.isArray(gestureConfig.activeOffsetX)
68
+ ? gestureConfig.activeOffsetX
69
+ : [-gestureConfig.activeOffsetX, gestureConfig.activeOffsetX]) as [number, number]
70
+ : (isHorizontal ? DEFAULT_ACTIVE_OFFSET_X : DEFAULT_ACTIVE_OFFSET_Y);
71
+
72
+ const activeOffsetY = gestureConfig?.activeOffsetY
73
+ ? (Array.isArray(gestureConfig.activeOffsetY)
74
+ ? gestureConfig.activeOffsetY
75
+ : [-gestureConfig.activeOffsetY, gestureConfig.activeOffsetY]) as [number, number]
76
+ : (isHorizontal ? DEFAULT_ACTIVE_OFFSET_Y : DEFAULT_ACTIVE_OFFSET_X);
77
+
78
+ return {
79
+ totalItems,
80
+ itemSize,
81
+ gap,
82
+ snapPoints,
83
+ isHorizontal,
84
+ loop,
85
+ enabled,
86
+ activeOffsetX,
87
+ activeOffsetY,
88
+ velocityThreshold: gestureConfig?.velocityThreshold ?? DEFAULT_VELOCITY_THRESHOLD,
89
+ onIndexChange,
90
+ onScrollStart,
91
+ onScrollEnd,
92
+ onConfigurePanGesture: gestureConfig?.onConfigurePanGesture,
93
+ };
94
+ }, [
95
+ totalItems,
96
+ itemSize,
97
+ gap,
98
+ snapPoints,
99
+ isHorizontal,
100
+ loop,
101
+ enabled,
102
+ gestureConfig,
103
+ onIndexChange,
104
+ onScrollStart,
105
+ onScrollEnd,
106
+ ]);
107
+
108
+ return usePanGesture(panOptions);
109
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @file useItemAnimation hook
3
+ * @description Applies animation preset to a carousel item based on scroll progress
4
+ */
5
+
6
+ import { useAnimatedStyle, type SharedValue } from 'react-native-reanimated';
7
+ import type { AnimationPresetFn, CustomAnimationFn } from '../types';
8
+ import { getPreset } from '../animations/registry';
9
+
10
+ /**
11
+ * Applies an animation preset to a carousel item.
12
+ * Returns an animated style that can be applied to an Animated.View.
13
+ *
14
+ * @param progress - Shared value with normalized animation progress
15
+ * @param preset - Preset name string or custom animation function
16
+ * @param animationConfig - Optional config overrides for the preset
17
+ * @param index - Item index (for custom animation functions)
18
+ * @param totalItems - Total number of items (for custom animation functions)
19
+ * @returns Animated style for the item
20
+ */
21
+ export const useItemAnimation = (
22
+ progress: SharedValue<number>,
23
+ preset: string | CustomAnimationFn | undefined,
24
+ animationConfig?: Record<string, number>,
25
+ index: number = 0,
26
+ totalItems: number = 0
27
+ ) => {
28
+ return useAnimatedStyle(() => {
29
+ if (!preset) {
30
+ return {};
31
+ }
32
+
33
+ if (typeof preset === 'function') {
34
+ return preset(progress.value, index, totalItems, animationConfig) as Record<string, unknown>;
35
+ }
36
+
37
+ const animFn = getPreset(preset) as AnimationPresetFn | undefined;
38
+ if (!animFn) {
39
+ return {};
40
+ }
41
+
42
+ return animFn(progress.value, animationConfig) as Record<string, unknown>;
43
+ }, [preset, animationConfig, index, totalItems]);
44
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file usePagination hook
3
+ * @description Manages pagination state derived from scroll progress
4
+ */
5
+
6
+ import { useDerivedValue, type SharedValue } from 'react-native-reanimated';
7
+
8
+ /** Return type of the pagination hook */
9
+ export interface UsePaginationReturn {
10
+ /** Current page index as a shared value (for UI thread usage) */
11
+ currentPage: SharedValue<number>;
12
+ }
13
+
14
+ /**
15
+ * Derives the current page index from scroll offset.
16
+ * Runs on the UI thread for smooth pagination indicator updates.
17
+ *
18
+ * @param scrollOffset - Current scroll offset shared value
19
+ * @param itemSize - Width or height of each item
20
+ * @param gap - Gap between items
21
+ * @param totalItems - Total number of items
22
+ * @returns Current page as shared value
23
+ */
24
+ export const usePagination = (
25
+ scrollOffset: SharedValue<number>,
26
+ itemSize: number,
27
+ gap: number,
28
+ totalItems: number
29
+ ): UsePaginationReturn => {
30
+ const stepSize = itemSize + gap;
31
+
32
+ const currentPage = useDerivedValue(() => {
33
+ if (stepSize === 0 || totalItems === 0) return 0;
34
+ const page = Math.round(scrollOffset.value / stepSize);
35
+ return Math.max(0, Math.min(page, totalItems - 1));
36
+ }, [stepSize, totalItems]);
37
+
38
+ return { currentPage };
39
+ };