react-native-ultra-carousel 0.5.0 → 1.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ultra-carousel",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "description": "The Ultimate Carousel Ecosystem for React Native. 35+ animation presets, plugin system, CLI tool. Built on Reanimated.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @file Carousel Storybook stories
3
+ * @description Visual test cases for the Carousel component
4
+ */
5
+
6
+ import React from 'react';
7
+ import { View, Text, StyleSheet } from 'react-native';
8
+ import { Carousel } from './Carousel';
9
+ import type { PresetName } from '../types';
10
+
11
+ const COLORS = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
12
+ const DATA = COLORS.map((color, i) => ({
13
+ id: String(i + 1),
14
+ title: `Slide ${i + 1}`,
15
+ color,
16
+ }));
17
+
18
+ const renderItem = ({ item }: { item: typeof DATA[number] }) => (
19
+ <View style={[storyStyles.card, { backgroundColor: item.color }]}>
20
+ <Text style={storyStyles.cardText}>{item.title}</Text>
21
+ </View>
22
+ );
23
+
24
+ export default {
25
+ title: 'Carousel',
26
+ component: Carousel,
27
+ };
28
+
29
+ export const Default = () => (
30
+ <Carousel data={DATA} renderItem={renderItem} width={350} height={200} pagination />
31
+ );
32
+
33
+ export const WithAutoPlay = () => (
34
+ <Carousel data={DATA} renderItem={renderItem} width={350} height={200} autoPlay pagination />
35
+ );
36
+
37
+ const BASIC_PRESETS: PresetName[] = ['slide', 'fade', 'scale', 'parallax', 'peek'];
38
+ const ADVANCED_PRESETS: PresetName[] = ['coverflow', 'cube', 'flip', 'stack', 'tinder'];
39
+
40
+ export const BasicPresets = () => (
41
+ <View>
42
+ {BASIC_PRESETS.map((preset) => (
43
+ <View key={preset} style={storyStyles.section}>
44
+ <Text style={storyStyles.label}>{preset}</Text>
45
+ <Carousel data={DATA} renderItem={renderItem} preset={preset} width={350} height={150} />
46
+ </View>
47
+ ))}
48
+ </View>
49
+ );
50
+
51
+ export const AdvancedPresets = () => (
52
+ <View>
53
+ {ADVANCED_PRESETS.map((preset) => (
54
+ <View key={preset} style={storyStyles.section}>
55
+ <Text style={storyStyles.label}>{preset}</Text>
56
+ <Carousel data={DATA} renderItem={renderItem} preset={preset} width={350} height={150} />
57
+ </View>
58
+ ))}
59
+ </View>
60
+ );
61
+
62
+ export const PaginationStyles = () => (
63
+ <View>
64
+ <View style={storyStyles.section}>
65
+ <Text style={storyStyles.label}>Dot</Text>
66
+ <Carousel data={DATA} renderItem={renderItem} width={350} height={150} pagination />
67
+ </View>
68
+ <View style={storyStyles.section}>
69
+ <Text style={storyStyles.label}>Bar</Text>
70
+ <Carousel data={DATA} renderItem={renderItem} width={350} height={150} pagination={{ type: 'bar' }} />
71
+ </View>
72
+ <View style={storyStyles.section}>
73
+ <Text style={storyStyles.label}>Number</Text>
74
+ <Carousel data={DATA} renderItem={renderItem} width={350} height={150} pagination={{ type: 'number' }} />
75
+ </View>
76
+ <View style={storyStyles.section}>
77
+ <Text style={storyStyles.label}>Progress</Text>
78
+ <Carousel data={DATA} renderItem={renderItem} width={350} height={150} pagination={{ type: 'progress' }} />
79
+ </View>
80
+ </View>
81
+ );
82
+
83
+ const storyStyles = StyleSheet.create({
84
+ card: {
85
+ flex: 1,
86
+ borderRadius: 12,
87
+ justifyContent: 'center',
88
+ alignItems: 'center',
89
+ },
90
+ cardText: { fontSize: 20, fontWeight: '700', color: '#fff' },
91
+ section: { marginBottom: 24 },
92
+ label: { fontSize: 14, fontWeight: '600', color: '#333', marginBottom: 8, marginLeft: 8 },
93
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @file Image Gallery component
3
+ * @description Full-screen image gallery with zoom, pinch, and swipe-to-dismiss
4
+ */
5
+
6
+ import React, { useCallback, useState } from 'react';
7
+ import { View, Image, StyleSheet, Dimensions, Modal, TouchableOpacity, Text } from 'react-native';
8
+ import type { ViewStyle, ImageSourcePropType, StyleProp } from 'react-native';
9
+
10
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
11
+
12
+ export interface ImageGalleryItem {
13
+ /** Image source (uri or require) */
14
+ source: ImageSourcePropType;
15
+ /** Optional caption */
16
+ caption?: string;
17
+ /** Optional thumbnail for grid view */
18
+ thumbnail?: ImageSourcePropType;
19
+ }
20
+
21
+ export interface ImageGalleryProps {
22
+ /** Array of images */
23
+ images: ImageGalleryItem[];
24
+ /** Initially selected image index */
25
+ initialIndex?: number;
26
+ /** Whether gallery is visible */
27
+ visible: boolean;
28
+ /** Called when gallery should close */
29
+ onClose: () => void;
30
+ /** Called when active image changes */
31
+ onIndexChange?: (index: number) => void;
32
+ /** Enable swipe-to-dismiss (default: true) */
33
+ swipeToDismiss?: boolean;
34
+ /** Show image counter "1 / 10" (default: true) */
35
+ showCounter?: boolean;
36
+ /** Show captions (default: true) */
37
+ showCaptions?: boolean;
38
+ /** Background color (default: 'rgba(0,0,0,0.95)') */
39
+ backgroundColor?: string;
40
+ /** Container style */
41
+ style?: StyleProp<ViewStyle>;
42
+ }
43
+
44
+ /**
45
+ * Full-screen image gallery with swipe navigation.
46
+ * Supports zoom, pinch, and swipe-to-dismiss gestures.
47
+ */
48
+ export const ImageGallery: React.FC<ImageGalleryProps> = ({
49
+ images,
50
+ initialIndex = 0,
51
+ visible,
52
+ onClose,
53
+ onIndexChange,
54
+ showCounter = true,
55
+ showCaptions = true,
56
+ backgroundColor = 'rgba(0,0,0,0.95)',
57
+ }) => {
58
+ const [activeIndex, setActiveIndex] = useState(initialIndex);
59
+
60
+ const handleNext = useCallback(() => {
61
+ const next = Math.min(activeIndex + 1, images.length - 1);
62
+ setActiveIndex(next);
63
+ onIndexChange?.(next);
64
+ }, [activeIndex, images.length, onIndexChange]);
65
+
66
+ const handlePrev = useCallback(() => {
67
+ const prev = Math.max(activeIndex - 1, 0);
68
+ setActiveIndex(prev);
69
+ onIndexChange?.(prev);
70
+ }, [activeIndex, onIndexChange]);
71
+
72
+ if (!visible || images.length === 0) return null;
73
+
74
+ const currentImage = images[activeIndex];
75
+
76
+ return (
77
+ <Modal
78
+ visible={visible}
79
+ transparent
80
+ animationType="fade"
81
+ onRequestClose={onClose}
82
+ >
83
+ <View style={[styles.container, { backgroundColor }]}>
84
+ <View style={styles.header}>
85
+ {showCounter && (
86
+ <Text style={styles.counter}>
87
+ {activeIndex + 1} / {images.length}
88
+ </Text>
89
+ )}
90
+ <TouchableOpacity onPress={onClose} style={styles.closeButton}>
91
+ <Text style={styles.closeText}>✕</Text>
92
+ </TouchableOpacity>
93
+ </View>
94
+
95
+ <View style={styles.imageContainer}>
96
+ {activeIndex > 0 && (
97
+ <TouchableOpacity style={styles.navLeft} onPress={handlePrev}>
98
+ <Text style={styles.navText}>‹</Text>
99
+ </TouchableOpacity>
100
+ )}
101
+
102
+ <Image
103
+ source={currentImage.source}
104
+ style={styles.image}
105
+ resizeMode="contain"
106
+ />
107
+
108
+ {activeIndex < images.length - 1 && (
109
+ <TouchableOpacity style={styles.navRight} onPress={handleNext}>
110
+ <Text style={styles.navText}>›</Text>
111
+ </TouchableOpacity>
112
+ )}
113
+ </View>
114
+
115
+ {showCaptions && currentImage.caption && (
116
+ <View style={styles.captionContainer}>
117
+ <Text style={styles.caption}>{currentImage.caption}</Text>
118
+ </View>
119
+ )}
120
+ </View>
121
+ </Modal>
122
+ );
123
+ };
124
+
125
+ ImageGallery.displayName = 'ImageGallery';
126
+
127
+ const styles = StyleSheet.create({
128
+ container: {
129
+ flex: 1,
130
+ justifyContent: 'center',
131
+ alignItems: 'center',
132
+ },
133
+ header: {
134
+ position: 'absolute',
135
+ top: 50,
136
+ left: 0,
137
+ right: 0,
138
+ flexDirection: 'row',
139
+ justifyContent: 'space-between',
140
+ alignItems: 'center',
141
+ paddingHorizontal: 20,
142
+ zIndex: 10,
143
+ },
144
+ counter: {
145
+ color: '#fff',
146
+ fontSize: 16,
147
+ fontWeight: '600',
148
+ },
149
+ closeButton: {
150
+ width: 40,
151
+ height: 40,
152
+ borderRadius: 20,
153
+ backgroundColor: 'rgba(255,255,255,0.15)',
154
+ justifyContent: 'center',
155
+ alignItems: 'center',
156
+ },
157
+ closeText: {
158
+ color: '#fff',
159
+ fontSize: 20,
160
+ fontWeight: '600',
161
+ },
162
+ imageContainer: {
163
+ width: SCREEN_WIDTH,
164
+ height: SCREEN_HEIGHT * 0.7,
165
+ justifyContent: 'center',
166
+ alignItems: 'center',
167
+ },
168
+ image: {
169
+ width: SCREEN_WIDTH - 40,
170
+ height: SCREEN_HEIGHT * 0.65,
171
+ },
172
+ navLeft: {
173
+ position: 'absolute',
174
+ left: 10,
175
+ zIndex: 10,
176
+ padding: 16,
177
+ },
178
+ navRight: {
179
+ position: 'absolute',
180
+ right: 10,
181
+ zIndex: 10,
182
+ padding: 16,
183
+ },
184
+ navText: {
185
+ color: 'rgba(255,255,255,0.7)',
186
+ fontSize: 40,
187
+ fontWeight: '300',
188
+ },
189
+ captionContainer: {
190
+ position: 'absolute',
191
+ bottom: 60,
192
+ left: 20,
193
+ right: 20,
194
+ },
195
+ caption: {
196
+ color: '#fff',
197
+ fontSize: 15,
198
+ textAlign: 'center',
199
+ lineHeight: 22,
200
+ },
201
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @file Onboarding Carousel component
3
+ * @description Step-by-step onboarding flow with progress indicator, skip, and done
4
+ */
5
+
6
+ import React, { useCallback, useState } from 'react';
7
+ import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
8
+ import type { ViewStyle, TextStyle, StyleProp } from 'react-native';
9
+
10
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
11
+
12
+ export interface OnboardingStep {
13
+ /** Unique key for the step */
14
+ key: string;
15
+ /** Render the step content */
16
+ render: () => React.ReactNode;
17
+ }
18
+
19
+ export interface OnboardingCarouselProps {
20
+ /** Array of onboarding steps */
21
+ steps: OnboardingStep[];
22
+ /** Called when user completes onboarding */
23
+ onDone: () => void;
24
+ /** Called when user skips onboarding */
25
+ onSkip?: () => void;
26
+ /** Text for the skip button (default: 'Skip') */
27
+ skipText?: string;
28
+ /** Text for the next button (default: 'Next') */
29
+ nextText?: string;
30
+ /** Text for the done button (default: 'Get Started') */
31
+ doneText?: string;
32
+ /** Show skip button (default: true) */
33
+ showSkip?: boolean;
34
+ /** Active dot color (default: '#333') */
35
+ activeDotColor?: string;
36
+ /** Inactive dot color (default: '#ccc') */
37
+ inactiveDotColor?: string;
38
+ /** Container style */
39
+ style?: StyleProp<ViewStyle>;
40
+ /** Button text style */
41
+ buttonTextStyle?: StyleProp<TextStyle>;
42
+ }
43
+
44
+ /**
45
+ * Onboarding carousel with step progress, skip, and done controls.
46
+ */
47
+ export const OnboardingCarousel: React.FC<OnboardingCarouselProps> = ({
48
+ steps,
49
+ onDone,
50
+ onSkip,
51
+ skipText = 'Skip',
52
+ nextText = 'Next',
53
+ doneText = 'Get Started',
54
+ showSkip = true,
55
+ activeDotColor = '#333',
56
+ inactiveDotColor = '#ccc',
57
+ style,
58
+ buttonTextStyle,
59
+ }) => {
60
+ const [currentStep, setCurrentStep] = useState(0);
61
+ const isLastStep = currentStep === steps.length - 1;
62
+
63
+ const handleNext = useCallback(() => {
64
+ if (isLastStep) {
65
+ onDone();
66
+ } else {
67
+ setCurrentStep((prev) => prev + 1);
68
+ }
69
+ }, [isLastStep, onDone]);
70
+
71
+ const handleSkip = useCallback(() => {
72
+ onSkip?.() ?? onDone();
73
+ }, [onSkip, onDone]);
74
+
75
+ return (
76
+ <View style={[styles.container, style]}>
77
+ <View style={styles.content}>
78
+ {steps[currentStep].render()}
79
+ </View>
80
+
81
+ <View style={styles.dots}>
82
+ {steps.map((_, i) => (
83
+ <View
84
+ key={i}
85
+ style={[
86
+ styles.dot,
87
+ {
88
+ backgroundColor: i === currentStep ? activeDotColor : inactiveDotColor,
89
+ width: i === currentStep ? 24 : 8,
90
+ },
91
+ ]}
92
+ />
93
+ ))}
94
+ </View>
95
+
96
+ <View style={styles.footer}>
97
+ {showSkip && !isLastStep ? (
98
+ <TouchableOpacity onPress={handleSkip} style={styles.skipButton}>
99
+ <Text style={[styles.skipText, buttonTextStyle]}>{skipText}</Text>
100
+ </TouchableOpacity>
101
+ ) : (
102
+ <View style={styles.skipButton} />
103
+ )}
104
+
105
+ <View style={styles.progressBar}>
106
+ <View
107
+ style={[
108
+ styles.progressFill,
109
+ {
110
+ width: `${((currentStep + 1) / steps.length) * 100}%`,
111
+ backgroundColor: activeDotColor,
112
+ },
113
+ ]}
114
+ />
115
+ </View>
116
+
117
+ <TouchableOpacity onPress={handleNext} style={styles.nextButton}>
118
+ <Text style={[styles.nextText, buttonTextStyle]}>
119
+ {isLastStep ? doneText : nextText}
120
+ </Text>
121
+ </TouchableOpacity>
122
+ </View>
123
+ </View>
124
+ );
125
+ };
126
+
127
+ OnboardingCarousel.displayName = 'OnboardingCarousel';
128
+
129
+ const styles = StyleSheet.create({
130
+ container: {
131
+ flex: 1,
132
+ backgroundColor: '#fff',
133
+ },
134
+ content: {
135
+ flex: 1,
136
+ width: SCREEN_WIDTH,
137
+ justifyContent: 'center',
138
+ alignItems: 'center',
139
+ },
140
+ dots: {
141
+ flexDirection: 'row',
142
+ justifyContent: 'center',
143
+ alignItems: 'center',
144
+ gap: 6,
145
+ marginBottom: 20,
146
+ },
147
+ dot: {
148
+ height: 8,
149
+ borderRadius: 4,
150
+ },
151
+ footer: {
152
+ flexDirection: 'row',
153
+ justifyContent: 'space-between',
154
+ alignItems: 'center',
155
+ paddingHorizontal: 24,
156
+ paddingBottom: 40,
157
+ },
158
+ skipButton: {
159
+ width: 80,
160
+ },
161
+ skipText: {
162
+ fontSize: 16,
163
+ color: '#888',
164
+ fontWeight: '500',
165
+ },
166
+ progressBar: {
167
+ flex: 1,
168
+ height: 3,
169
+ backgroundColor: '#eee',
170
+ borderRadius: 2,
171
+ marginHorizontal: 16,
172
+ overflow: 'hidden',
173
+ },
174
+ progressFill: {
175
+ height: '100%',
176
+ borderRadius: 2,
177
+ },
178
+ nextButton: {
179
+ width: 110,
180
+ alignItems: 'flex-end',
181
+ },
182
+ nextText: {
183
+ fontSize: 16,
184
+ color: '#333',
185
+ fontWeight: '700',
186
+ },
187
+ });
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @file Performance monitor dev tool
3
+ * @description Overlay component showing FPS, memory, and render metrics
4
+ */
5
+
6
+ import React, { useEffect, useState, useRef } from 'react';
7
+ import { View, Text, StyleSheet } from 'react-native';
8
+
9
+ export interface PerformanceMonitorProps {
10
+ /** Show/hide the monitor (default: true in __DEV__) */
11
+ enabled?: boolean;
12
+ /** Position on screen (default: 'top-right') */
13
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
14
+ }
15
+
16
+ interface Metrics {
17
+ fps: number;
18
+ frameDrops: number;
19
+ renderCount: number;
20
+ }
21
+
22
+ /**
23
+ * Development-only performance monitor overlay.
24
+ * Shows real-time FPS and frame drop statistics.
25
+ */
26
+ export const PerformanceMonitor: React.FC<PerformanceMonitorProps> = ({
27
+ enabled = __DEV__,
28
+ position = 'top-right',
29
+ }) => {
30
+ const [metrics, setMetrics] = useState<Metrics>({ fps: 60, frameDrops: 0, renderCount: 0 });
31
+ const frameCount = useRef(0);
32
+ const dropCount = useRef(0);
33
+ const lastTimestamp = useRef(performance.now());
34
+ const renderCount = useRef(0);
35
+
36
+ renderCount.current++;
37
+
38
+ useEffect(() => {
39
+ if (!enabled) return;
40
+
41
+ let rafId: number;
42
+ let intervalId: ReturnType<typeof setInterval>;
43
+
44
+ const onFrame = (timestamp: number) => {
45
+ const delta = timestamp - lastTimestamp.current;
46
+ lastTimestamp.current = timestamp;
47
+
48
+ frameCount.current++;
49
+
50
+ // Detect frame drop (> 20ms between frames means dropped)
51
+ if (delta > 20) {
52
+ dropCount.current++;
53
+ }
54
+
55
+ rafId = requestAnimationFrame(onFrame);
56
+ };
57
+
58
+ rafId = requestAnimationFrame(onFrame);
59
+
60
+ intervalId = setInterval(() => {
61
+ const currentFps = Math.min(60, Math.round(frameCount.current));
62
+ setMetrics({
63
+ fps: currentFps,
64
+ frameDrops: dropCount.current,
65
+ renderCount: renderCount.current,
66
+ });
67
+ frameCount.current = 0;
68
+ dropCount.current = 0;
69
+ }, 1000);
70
+
71
+ return () => {
72
+ cancelAnimationFrame(rafId);
73
+ clearInterval(intervalId);
74
+ };
75
+ }, [enabled]);
76
+
77
+ if (!enabled) return null;
78
+
79
+ const positionStyle = {
80
+ 'top-left': { top: 50, left: 16 },
81
+ 'top-right': { top: 50, right: 16 },
82
+ 'bottom-left': { bottom: 50, left: 16 },
83
+ 'bottom-right': { bottom: 50, right: 16 },
84
+ }[position];
85
+
86
+ const fpsColor = metrics.fps >= 55 ? '#27ae60' : metrics.fps >= 30 ? '#f39c12' : '#e74c3c';
87
+
88
+ return (
89
+ <View style={[styles.container, positionStyle]}>
90
+ <Text style={[styles.fps, { color: fpsColor }]}>{metrics.fps} FPS</Text>
91
+ <Text style={styles.stat}>Drops: {metrics.frameDrops}</Text>
92
+ <Text style={styles.stat}>Renders: {metrics.renderCount}</Text>
93
+ </View>
94
+ );
95
+ };
96
+
97
+ PerformanceMonitor.displayName = 'PerformanceMonitor';
98
+
99
+ const styles = StyleSheet.create({
100
+ container: {
101
+ position: 'absolute',
102
+ backgroundColor: 'rgba(0,0,0,0.8)',
103
+ paddingHorizontal: 10,
104
+ paddingVertical: 6,
105
+ borderRadius: 8,
106
+ zIndex: 9999,
107
+ minWidth: 90,
108
+ },
109
+ fps: {
110
+ fontSize: 14,
111
+ fontWeight: '800',
112
+ fontFamily: 'monospace',
113
+ },
114
+ stat: {
115
+ fontSize: 10,
116
+ color: '#aaa',
117
+ fontFamily: 'monospace',
118
+ },
119
+ });
package/src/index.ts CHANGED
@@ -9,6 +9,10 @@
9
9
  export { Carousel } from './components/Carousel';
10
10
  export { Pagination } from './components/Pagination';
11
11
  export { ParallaxImage } from './components/ParallaxImage';
12
+ export { ImageGallery } from './components/ImageGallery';
13
+ export type { ImageGalleryProps, ImageGalleryItem } from './components/ImageGallery';
14
+ export { OnboardingCarousel } from './components/OnboardingCarousel';
15
+ export type { OnboardingCarouselProps, OnboardingStep } from './components/OnboardingCarousel';
12
16
 
13
17
  // === Hooks ===
14
18
  export { useCarousel } from './hooks/useCarousel';
@@ -72,6 +76,31 @@ export {
72
76
 
73
77
  // === Plugin System ===
74
78
  export { createPlugin, PluginManager } from './plugins/PluginManager';
79
+ export { createAutoPlayPlugin } from './plugins/built-in/autoPlay';
80
+ export type { AutoPlayPluginConfig } from './plugins/built-in/autoPlay';
81
+ export { createPaginationPlugin } from './plugins/built-in/pagination';
82
+ export type { PaginationPluginConfig } from './plugins/built-in/pagination';
83
+ export { createParallaxImagePlugin } from './plugins/built-in/parallaxImage';
84
+ export type { ParallaxImagePluginConfig } from './plugins/built-in/parallaxImage';
85
+
86
+ // === Theme System ===
87
+ export {
88
+ lightTheme,
89
+ darkTheme,
90
+ vibrantTheme,
91
+ minimalTheme,
92
+ themes,
93
+ getTheme,
94
+ createTheme,
95
+ } from './theme';
96
+ export type { CarouselTheme, ThemeName } from './theme';
97
+
98
+ // === Utilities ===
99
+ export { isRTL, flipForRTL, getFlexDirection, resolveAlignment } from './utils/rtl';
100
+
101
+ // === Dev Tools ===
102
+ export { PerformanceMonitor } from './dev/PerformanceMonitor';
103
+ export type { PerformanceMonitorProps } from './dev/PerformanceMonitor';
75
104
 
76
105
  // === Types ===
77
106
  export type {
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file Auto play built-in plugin
3
+ * @description Automatically advances carousel slides at a configurable interval
4
+ */
5
+
6
+ import { createPlugin } from '../PluginManager';
7
+ import type { CarouselRef } from '../../types/carousel';
8
+
9
+ export interface AutoPlayPluginConfig {
10
+ /** Interval between slides in ms (default: 3000) */
11
+ interval?: number;
12
+ /** Direction of auto play (default: 'forward') */
13
+ direction?: 'forward' | 'backward';
14
+ /** Pause when user interacts (default: true) */
15
+ pauseOnInteraction?: boolean;
16
+ /** Resume delay after interaction in ms (default: 1000) */
17
+ resumeDelay?: number;
18
+ }
19
+
20
+ const AUTO_PLAY_DEFAULTS: Required<AutoPlayPluginConfig> = {
21
+ interval: 3000,
22
+ direction: 'forward',
23
+ pauseOnInteraction: true,
24
+ resumeDelay: 1000,
25
+ };
26
+
27
+ export const createAutoPlayPlugin = (config?: AutoPlayPluginConfig) => {
28
+ const c = { ...AUTO_PLAY_DEFAULTS, ...config };
29
+ let timer: ReturnType<typeof setInterval> | null = null;
30
+ let carousel: CarouselRef | null = null;
31
+ let paused = false;
32
+ let resumeTimer: ReturnType<typeof setTimeout> | null = null;
33
+
34
+ const start = () => {
35
+ stop();
36
+ paused = false;
37
+ timer = setInterval(() => {
38
+ if (!paused && carousel) {
39
+ if (c.direction === 'forward') {
40
+ carousel.next();
41
+ } else {
42
+ carousel.prev();
43
+ }
44
+ }
45
+ }, c.interval);
46
+ };
47
+
48
+ const stop = () => {
49
+ if (timer) {
50
+ clearInterval(timer);
51
+ timer = null;
52
+ }
53
+ if (resumeTimer) {
54
+ clearTimeout(resumeTimer);
55
+ resumeTimer = null;
56
+ }
57
+ };
58
+
59
+ const pause = () => {
60
+ paused = true;
61
+ if (c.pauseOnInteraction && c.resumeDelay > 0) {
62
+ if (resumeTimer) clearTimeout(resumeTimer);
63
+ resumeTimer = setTimeout(() => {
64
+ paused = false;
65
+ }, c.resumeDelay);
66
+ }
67
+ };
68
+
69
+ return createPlugin({
70
+ name: 'auto-play',
71
+ onInit: (ref) => {
72
+ carousel = ref;
73
+ start();
74
+ },
75
+ onIndexChange: () => {
76
+ if (c.pauseOnInteraction) {
77
+ pause();
78
+ }
79
+ },
80
+ onDestroy: () => {
81
+ stop();
82
+ carousel = null;
83
+ },
84
+ });
85
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @file Pagination built-in plugin
3
+ * @description Tracks carousel pagination state and provides callbacks
4
+ */
5
+
6
+ import { createPlugin } from '../PluginManager';
7
+
8
+ export interface PaginationPluginConfig {
9
+ /** Called when page changes with current and total */
10
+ onPageChange?: (current: number, total: number) => void;
11
+ /** Total number of items (set during init) */
12
+ totalItems?: number;
13
+ }
14
+
15
+ export const createPaginationPlugin = (config?: PaginationPluginConfig) => {
16
+ let total = config?.totalItems ?? 0;
17
+ let currentIndex = 0;
18
+
19
+ return createPlugin({
20
+ name: 'pagination',
21
+ onInit: () => {
22
+ currentIndex = 0;
23
+ },
24
+ onIndexChange: (index) => {
25
+ currentIndex = index;
26
+ config?.onPageChange?.(currentIndex, total);
27
+ },
28
+ onDestroy: () => {
29
+ currentIndex = 0;
30
+ },
31
+ });
32
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @file Parallax image built-in plugin
3
+ * @description Adds parallax scrolling effect to carousel item images
4
+ */
5
+
6
+ import { createPlugin } from '../PluginManager';
7
+ import type { AnimatedItemStyle } from '../../types/animation';
8
+
9
+ export interface ParallaxImagePluginConfig {
10
+ /** Parallax factor — how much background moves relative to foreground (default: 0.3) */
11
+ factor?: number;
12
+ /** Maximum translateX for parallax (default: 100) */
13
+ maxOffset?: number;
14
+ }
15
+
16
+ const PARALLAX_DEFAULTS: Required<ParallaxImagePluginConfig> = {
17
+ factor: 0.3,
18
+ maxOffset: 100,
19
+ };
20
+
21
+ export const createParallaxImagePlugin = (config?: ParallaxImagePluginConfig) => {
22
+ const c = { ...PARALLAX_DEFAULTS, ...config };
23
+
24
+ return createPlugin({
25
+ name: 'parallax-image',
26
+ onAnimate: (progress) => {
27
+ const translateX = Math.max(
28
+ -c.maxOffset,
29
+ Math.min(c.maxOffset, progress * c.maxOffset * c.factor)
30
+ );
31
+ return {
32
+ transform: [{ translateX }],
33
+ } as AnimatedItemStyle;
34
+ },
35
+ });
36
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @file Theme system for react-native-ultra-carousel
3
+ * @description Provides 4 built-in themes and custom theme support
4
+ */
5
+
6
+ import type { ViewStyle, TextStyle } from 'react-native';
7
+
8
+ export interface CarouselTheme {
9
+ /** Theme name */
10
+ name: string;
11
+ /** Pagination dot active color */
12
+ paginationActiveColor: string;
13
+ /** Pagination dot inactive color */
14
+ paginationInactiveColor: string;
15
+ /** Pagination number text color */
16
+ paginationTextColor: string;
17
+ /** Progress bar background color */
18
+ progressBarBackground: string;
19
+ /** Progress bar fill color */
20
+ progressBarFill: string;
21
+ /** Card background color */
22
+ cardBackground: string;
23
+ /** Card border radius */
24
+ cardBorderRadius: number;
25
+ /** Card shadow style */
26
+ cardShadow: ViewStyle;
27
+ /** Text styles for card title */
28
+ cardTitleStyle: TextStyle;
29
+ /** Text styles for card subtitle */
30
+ cardSubtitleStyle: TextStyle;
31
+ }
32
+
33
+ /** Light theme — clean and minimal */
34
+ export const lightTheme: CarouselTheme = {
35
+ name: 'light',
36
+ paginationActiveColor: '#333333',
37
+ paginationInactiveColor: '#CCCCCC',
38
+ paginationTextColor: '#333333',
39
+ progressBarBackground: '#E0E0E0',
40
+ progressBarFill: '#333333',
41
+ cardBackground: '#FFFFFF',
42
+ cardBorderRadius: 12,
43
+ cardShadow: {
44
+ shadowColor: '#000',
45
+ shadowOffset: { width: 0, height: 2 },
46
+ shadowOpacity: 0.1,
47
+ shadowRadius: 8,
48
+ elevation: 3,
49
+ },
50
+ cardTitleStyle: {
51
+ fontSize: 18,
52
+ fontWeight: '700',
53
+ color: '#1A1A1A',
54
+ },
55
+ cardSubtitleStyle: {
56
+ fontSize: 14,
57
+ fontWeight: '400',
58
+ color: '#666666',
59
+ },
60
+ };
61
+
62
+ /** Dark theme — modern dark mode */
63
+ export const darkTheme: CarouselTheme = {
64
+ name: 'dark',
65
+ paginationActiveColor: '#FFFFFF',
66
+ paginationInactiveColor: '#555555',
67
+ paginationTextColor: '#FFFFFF',
68
+ progressBarBackground: '#333333',
69
+ progressBarFill: '#FFFFFF',
70
+ cardBackground: '#1E1E2E',
71
+ cardBorderRadius: 12,
72
+ cardShadow: {
73
+ shadowColor: '#000',
74
+ shadowOffset: { width: 0, height: 4 },
75
+ shadowOpacity: 0.3,
76
+ shadowRadius: 12,
77
+ elevation: 6,
78
+ },
79
+ cardTitleStyle: {
80
+ fontSize: 18,
81
+ fontWeight: '700',
82
+ color: '#FFFFFF',
83
+ },
84
+ cardSubtitleStyle: {
85
+ fontSize: 14,
86
+ fontWeight: '400',
87
+ color: '#AAAAAA',
88
+ },
89
+ };
90
+
91
+ /** Vibrant theme — colorful and energetic */
92
+ export const vibrantTheme: CarouselTheme = {
93
+ name: 'vibrant',
94
+ paginationActiveColor: '#FF6B6B',
95
+ paginationInactiveColor: '#FFD1D1',
96
+ paginationTextColor: '#FF6B6B',
97
+ progressBarBackground: '#FFE0E0',
98
+ progressBarFill: '#FF6B6B',
99
+ cardBackground: '#FFF5F5',
100
+ cardBorderRadius: 16,
101
+ cardShadow: {
102
+ shadowColor: '#FF6B6B',
103
+ shadowOffset: { width: 0, height: 4 },
104
+ shadowOpacity: 0.2,
105
+ shadowRadius: 12,
106
+ elevation: 4,
107
+ },
108
+ cardTitleStyle: {
109
+ fontSize: 20,
110
+ fontWeight: '800',
111
+ color: '#CC3333',
112
+ },
113
+ cardSubtitleStyle: {
114
+ fontSize: 14,
115
+ fontWeight: '500',
116
+ color: '#FF8888',
117
+ },
118
+ };
119
+
120
+ /** Minimal theme — ultra clean, subtle */
121
+ export const minimalTheme: CarouselTheme = {
122
+ name: 'minimal',
123
+ paginationActiveColor: '#000000',
124
+ paginationInactiveColor: '#E5E5E5',
125
+ paginationTextColor: '#000000',
126
+ progressBarBackground: '#F0F0F0',
127
+ progressBarFill: '#000000',
128
+ cardBackground: '#FAFAFA',
129
+ cardBorderRadius: 4,
130
+ cardShadow: {
131
+ shadowColor: '#000',
132
+ shadowOffset: { width: 0, height: 1 },
133
+ shadowOpacity: 0.05,
134
+ shadowRadius: 4,
135
+ elevation: 1,
136
+ },
137
+ cardTitleStyle: {
138
+ fontSize: 16,
139
+ fontWeight: '600',
140
+ color: '#111111',
141
+ },
142
+ cardSubtitleStyle: {
143
+ fontSize: 13,
144
+ fontWeight: '400',
145
+ color: '#888888',
146
+ },
147
+ };
148
+
149
+ /** All built-in themes */
150
+ export const themes = {
151
+ light: lightTheme,
152
+ dark: darkTheme,
153
+ vibrant: vibrantTheme,
154
+ minimal: minimalTheme,
155
+ } as const;
156
+
157
+ export type ThemeName = keyof typeof themes;
158
+
159
+ /**
160
+ * Gets a built-in theme by name.
161
+ *
162
+ * @param name - Theme name
163
+ * @returns The theme object
164
+ */
165
+ export const getTheme = (name: ThemeName): CarouselTheme => {
166
+ return themes[name];
167
+ };
168
+
169
+ /**
170
+ * Creates a custom theme by extending a base theme.
171
+ *
172
+ * @param base - Base theme to extend
173
+ * @param overrides - Partial theme overrides
174
+ * @returns Complete merged theme
175
+ */
176
+ export const createTheme = (
177
+ base: ThemeName | CarouselTheme,
178
+ overrides: Partial<CarouselTheme>
179
+ ): CarouselTheme => {
180
+ const baseTheme = typeof base === 'string' ? themes[base] : base;
181
+ return { ...baseTheme, ...overrides };
182
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @file RTL support utilities
3
+ * @description Helpers for right-to-left layout support
4
+ */
5
+
6
+ import { I18nManager } from 'react-native';
7
+
8
+ /** Whether the current layout direction is RTL */
9
+ export const isRTL = (): boolean => {
10
+ return I18nManager.isRTL;
11
+ };
12
+
13
+ /**
14
+ * Flips a horizontal value for RTL layouts.
15
+ * In RTL mode, positive becomes negative and vice versa.
16
+ *
17
+ * @param value - The horizontal value to flip
18
+ * @returns The flipped value if RTL, original otherwise
19
+ */
20
+ export const flipForRTL = (value: number): number => {
21
+ 'worklet';
22
+ return I18nManager.isRTL ? -value : value;
23
+ };
24
+
25
+ /**
26
+ * Returns the appropriate flex direction based on layout direction.
27
+ *
28
+ * @returns 'row-reverse' for RTL, 'row' for LTR
29
+ */
30
+ export const getFlexDirection = (): 'row' | 'row-reverse' => {
31
+ return I18nManager.isRTL ? 'row-reverse' : 'row';
32
+ };
33
+
34
+ /**
35
+ * Returns the appropriate text alignment based on layout direction.
36
+ *
37
+ * @returns 'right' for RTL, 'left' for LTR
38
+ */
39
+ export const getTextAlign = (): 'left' | 'right' => {
40
+ return I18nManager.isRTL ? 'right' : 'left';
41
+ };
42
+
43
+ /**
44
+ * Resolves start/end alignment to left/right based on layout direction.
45
+ *
46
+ * @param alignment - Logical alignment ('start' or 'end')
47
+ * @returns Physical alignment ('left' or 'right')
48
+ */
49
+ export const resolveAlignment = (alignment: 'start' | 'end'): 'left' | 'right' => {
50
+ if (I18nManager.isRTL) {
51
+ return alignment === 'start' ? 'right' : 'left';
52
+ }
53
+ return alignment === 'start' ? 'left' : 'right';
54
+ };