@r0b0t3d/react-native-carousel 3.4.6 → 3.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/lib/commonjs/components/Carousel.js +290 -195
  2. package/lib/commonjs/components/Carousel.js.map +1 -1
  3. package/lib/commonjs/components/CarouselContainer.js +17 -31
  4. package/lib/commonjs/components/CarouselContainer.js.map +1 -1
  5. package/lib/commonjs/components/PageItem.js +52 -45
  6. package/lib/commonjs/components/PageItem.js.map +1 -1
  7. package/lib/commonjs/components/PaginationIndicator.js +23 -34
  8. package/lib/commonjs/components/PaginationIndicator.js.map +1 -1
  9. package/lib/commonjs/components/useCarouselContext.js +3 -8
  10. package/lib/commonjs/components/useCarouselContext.js.map +1 -1
  11. package/lib/commonjs/components/useInternalCarouselContext.js +3 -6
  12. package/lib/commonjs/components/useInternalCarouselContext.js.map +1 -1
  13. package/lib/commonjs/index.js +7 -17
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/package.json +1 -0
  16. package/lib/commonjs/types.js +4 -0
  17. package/lib/commonjs/types.js.map +1 -1
  18. package/lib/commonjs/utils.js +24 -18
  19. package/lib/commonjs/utils.js.map +1 -1
  20. package/lib/module/components/Carousel.js +291 -180
  21. package/lib/module/components/Carousel.js.map +1 -1
  22. package/lib/module/components/CarouselContainer.js +18 -23
  23. package/lib/module/components/CarouselContainer.js.map +1 -1
  24. package/lib/module/components/PageItem.js +53 -37
  25. package/lib/module/components/PageItem.js.map +1 -1
  26. package/lib/module/components/PaginationIndicator.js +23 -24
  27. package/lib/module/components/PaginationIndicator.js.map +1 -1
  28. package/lib/module/components/useCarouselContext.js +2 -2
  29. package/lib/module/components/useCarouselContext.js.map +1 -1
  30. package/lib/module/components/useInternalCarouselContext.js +2 -0
  31. package/lib/module/components/useInternalCarouselContext.js.map +1 -1
  32. package/lib/module/index.js +2 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/types.js +2 -0
  35. package/lib/module/types.js.map +1 -1
  36. package/lib/module/utils.js +22 -12
  37. package/lib/module/utils.js.map +1 -1
  38. package/lib/typescript/components/Carousel.d.ts +3 -1
  39. package/lib/typescript/components/Carousel.d.ts.map +1 -0
  40. package/lib/typescript/components/CarouselContainer.d.ts +3 -2
  41. package/lib/typescript/components/CarouselContainer.d.ts.map +1 -0
  42. package/lib/typescript/components/PageItem.d.ts +5 -3
  43. package/lib/typescript/components/PageItem.d.ts.map +1 -0
  44. package/lib/typescript/components/PaginationIndicator.d.ts +3 -1
  45. package/lib/typescript/components/PaginationIndicator.d.ts.map +1 -0
  46. package/lib/typescript/components/useCarouselContext.d.ts +1 -1
  47. package/lib/typescript/components/useCarouselContext.d.ts.map +1 -0
  48. package/lib/typescript/components/useInternalCarouselContext.d.ts +1 -1
  49. package/lib/typescript/components/useInternalCarouselContext.d.ts.map +1 -0
  50. package/lib/typescript/index.d.ts +1 -0
  51. package/lib/typescript/index.d.ts.map +1 -0
  52. package/lib/typescript/types.d.ts +17 -15
  53. package/lib/typescript/types.d.ts.map +1 -0
  54. package/lib/typescript/utils.d.ts +4 -1
  55. package/lib/typescript/utils.d.ts.map +1 -0
  56. package/package.json +26 -29
  57. package/src/components/Carousel.tsx +407 -206
  58. package/src/components/CarouselContainer.tsx +4 -4
  59. package/src/components/PageItem.tsx +55 -33
  60. package/src/components/PaginationIndicator.tsx +2 -2
  61. package/src/components/useCarouselContext.ts +0 -1
  62. package/src/components/useInternalCarouselContext.ts +0 -1
  63. package/src/types.ts +9 -7
  64. package/src/utils.ts +31 -11
@@ -4,35 +4,48 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
-
8
7
  var _react = _interopRequireWildcard(require("react"));
9
-
10
8
  var _reactNative = require("react-native");
11
-
12
9
  var _reactNativeHooks = require("@r0b0t3d/react-native-hooks");
13
-
14
10
  var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
15
-
11
+ var _reactNativeWorklets = require("react-native-worklets");
16
12
  var _PageItem = _interopRequireDefault(require("./PageItem"));
17
-
18
13
  var _utils = require("../utils");
19
-
20
14
  var _useCarouselContext = require("./useCarouselContext");
21
-
22
15
  var _useInternalCarouselContext = require("./useInternalCarouselContext");
23
-
24
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25
-
26
- function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
27
-
28
- function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
29
-
30
- function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
31
-
16
+ var _jsxRuntime = require("react/jsx-runtime");
17
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
19
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
20
+ // ---------------------------------------------------------------------------
21
+ // Carousel state machine
22
+ // ---------------------------------------------------------------------------
23
+ // Transitions:
24
+ //
25
+ // IDLE ── onBeginDrag ──► DRAGGING
26
+ // DRAGGING ── onMomentumEnd ──► IDLE (or LOOP_JUMP at boundary)
27
+ // LOOP_JUMP ── onScroll (arrival) ──► IDLE
28
+ // LOOP_JUMP ── onBeginDrag (user overrides) ──► DRAGGING
29
+ //
30
+ // Page change (onPageChange / animatedPage) only fires at IDLE transitions.
31
+ // Autoplay only advances when state === IDLE.
32
+ // ---------------------------------------------------------------------------
33
+ var CarouselState = /*#__PURE__*/function (CarouselState) {
34
+ CarouselState[CarouselState["IDLE"] = 0] = "IDLE";
35
+ CarouselState[CarouselState["DRAGGING"] = 1] = "DRAGGING";
36
+ CarouselState[CarouselState["LOOP_JUMP"] = 2] = "LOOP_JUMP";
37
+ return CarouselState;
38
+ }(CarouselState || {}); // ---------------------------------------------------------------------------
39
+ // Terminology
40
+ // ---------------------------------------------------------------------------
41
+ // render index – position in the padded pageItems array (includes loop
42
+ // clones at head/tail). Used for scroll offsets.
43
+ // logical index – position in the original data array (0 .. data.length-1).
44
+ // All consumer-facing callbacks receive logical indices.
45
+ // ---------------------------------------------------------------------------
32
46
  const getScreenWidth = () => {
33
47
  return _reactNative.Dimensions.get('screen').width;
34
48
  };
35
-
36
49
  function Carousel({
37
50
  style,
38
51
  data,
@@ -53,26 +66,36 @@ function Carousel({
53
66
  onPageChange,
54
67
  scrollViewProps = {},
55
68
  keyExtractor,
56
- onItemPress
69
+ onItemPress,
70
+ disableItemPress = false,
71
+ scrollViewRef: externalScrollViewRef
57
72
  }) {
58
- const currentPage = (0, _reactNativeReanimated.useSharedValue)(0);
73
+ // ---- shared values (UI-thread state) ------------------------------------
74
+
75
+ const currentRenderPage = (0, _reactNativeReanimated.useSharedValue)(0);
76
+ const currentLogicalPage = (0, _reactNativeReanimated.useSharedValue)(0);
59
77
  const animatedScroll = (0, _reactNativeReanimated.useSharedValue)(0);
78
+ const carouselState = (0, _reactNativeReanimated.useSharedValue)(CarouselState.IDLE);
79
+ // Destination render page for a loop jump. Used to detect arrival via
80
+ // onScroll and release the jump lock.
81
+ const jumpDestination = (0, _reactNativeReanimated.useSharedValue)(-1);
82
+ // Suppresses parallax/opacity animations during instant scrolls on
83
+ // Android where the scroll isn't truly synchronous.
60
84
  const freeze = (0, _reactNativeReanimated.useSharedValue)(false);
61
- const [isDragging, setDragging] = (0, _react.useState)(false);
62
- const expectedPosition = (0, _reactNativeReanimated.useSharedValue)(-1);
63
- const pageMapper = (0, _react.useRef)({});
85
+ // When false, autoplay is suppressed (app backgrounded).
86
+ const appActive = (0, _reactNativeReanimated.useSharedValue)(true);
87
+ const internalAnimatedRef = (0, _reactNativeReanimated.useAnimatedRef)();
64
88
  const {
65
89
  currentPage: animatedPage,
66
90
  totalPage
67
91
  } = (0, _useCarouselContext.useCarouselContext)();
68
- (0, _react.useEffect)(() => {
69
- animatedScroll.value = currentPage.value * sliderWidth;
70
- }, []);
92
+
93
+ // ---- derived values -----------------------------------------------------
94
+
71
95
  const horizontalPadding = (0, _react.useMemo)(() => {
72
96
  const padding = (sliderWidth - itemWidth) / 2;
73
97
  return firstItemAlignment === 'center' || loop ? padding : spaceHeadTail;
74
98
  }, [sliderWidth, itemWidth, firstItemAlignment, loop, spaceHeadTail]);
75
- const scrollViewRef = (0, _reactNativeReanimated.useAnimatedRef)();
76
99
  const offsets = (0, _react.useMemo)(() => {
77
100
  return (0, _utils.generateOffsets)({
78
101
  sliderWidth,
@@ -80,89 +103,122 @@ function Carousel({
80
103
  itemCount: data.length + (loop ? additionalPagesPerSide * 2 : 0),
81
104
  horizontalPadding
82
105
  });
83
- }, [sliderWidth, itemWidth, data, horizontalPadding]);
106
+ }, [sliderWidth, itemWidth, data.length, loop, additionalPagesPerSide, horizontalPadding]);
84
107
  const pageItems = (0, _react.useMemo)(() => {
85
- if (!data) {
108
+ if (!data || data.length === 0) {
86
109
  return [];
87
110
  }
88
-
89
- totalPage.value = data.length;
90
-
91
111
  if (loop) {
92
112
  const headItems = data.slice(data.length - additionalPagesPerSide, data.length);
93
113
  const tailItems = data.slice(0, additionalPagesPerSide);
94
- const newItems = [...headItems, ...data, ...tailItems];
114
+ return [...headItems, ...data, ...tailItems];
115
+ }
116
+ return data;
117
+ }, [data, loop, additionalPagesPerSide]);
95
118
 
96
- for (let i = 0; i < newItems.length; i++) {
97
- pageMapper.current[i] = (data.length - additionalPagesPerSide + i) % data.length;
98
- }
119
+ // ---- side effects -------------------------------------------------------
99
120
 
100
- return newItems;
101
- } else {
102
- for (let i = 0; i < data.length; i++) {
103
- pageMapper.current[i] = i;
104
- }
121
+ (0, _react.useEffect)(() => {
122
+ totalPage.value = data.length;
123
+ }, [data.length, totalPage]);
105
124
 
106
- return data;
125
+ // When the carousel layout or data changes (orientation, split-screen,
126
+ // toggling loop, data shrinking), reposition so the current logical
127
+ // page stays visible at its new render index and scroll offset.
128
+ (0, _react.useEffect)(() => {
129
+ if (data.length === 0 || offsets.length === 0) return;
130
+
131
+ // Clamp logical page in case data shrank below current position.
132
+ const clampedLogical = Math.min(currentLogicalPage.value, data.length - 1);
133
+ const renderIdx = loop ? clampedLogical + additionalPagesPerSide : clampedLogical;
134
+
135
+ // Defensive: ensure render index is within bounds.
136
+ const safeRenderIdx = Math.max(0, Math.min(renderIdx, offsets.length - 1));
137
+ const safeLogicalIdx = (0, _utils.getLogicalPage)(safeRenderIdx, data.length, additionalPagesPerSide, loop);
138
+ currentRenderPage.value = safeRenderIdx;
139
+ currentLogicalPage.value = safeLogicalIdx;
140
+ animatedPage.value = safeLogicalIdx;
141
+ (0, _reactNativeWorklets.scheduleOnUI)(idx => {
142
+ 'worklet';
143
+
144
+ (0, _reactNativeReanimated.scrollTo)(internalAnimatedRef, offsets[idx], 0, false);
145
+ }, safeRenderIdx);
146
+ animatedScroll.value = offsets[safeRenderIdx] ?? 0;
147
+ // Fire onPageChange if logical page actually changed (e.g. data shrank).
148
+ if (safeLogicalIdx !== currentLogicalPage.value) {
149
+ (0, _reactNativeWorklets.scheduleOnRN)(handlePageChange, safeLogicalIdx);
107
150
  }
108
- }, [data, loop]);
109
- const getActualPage = (0, _react.useCallback)(page => {
110
- return pageMapper.current[page];
111
- }, []);
112
- const getRef = (0, _react.useCallback)(() => {
113
- if (!scrollViewRef.current) return;
151
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
+ }, [data.length, sliderWidth, itemWidth, loop, additionalPagesPerSide]);
153
+
154
+ // ---- app-state listener (pause autoplay when backgrounded) --------------
155
+
156
+ (0, _react.useEffect)(() => {
157
+ const sub = _reactNative.AppState.addEventListener('change', state => {
158
+ appActive.value = state === 'active';
159
+ });
160
+ return () => sub.remove();
161
+ }, [appActive]);
162
+
163
+ // ---- page-change callback (JS thread) -----------------------------------
114
164
 
115
- if (scrollViewRef.current.scrollTo) {
116
- return scrollViewRef.current;
165
+ const handlePageChange = (0, _react.useCallback)(logicalPage => {
166
+ animatedPage.value = logicalPage;
167
+ if (onPageChange) {
168
+ onPageChange(logicalPage);
117
169
  }
170
+ }, [onPageChange, animatedPage]);
118
171
 
119
- return scrollViewRef.current.getNode();
120
- }, []);
172
+ // ---- scroll helpers -----------------------------------------------------
173
+
174
+ // Safe accessor: returns offset for a render index, clamping to valid
175
+ // range and falling back to 0 if offsets is empty.
176
+ const renderOffset = (0, _react.useCallback)(renderIdx => {
177
+ if (offsets.length === 0) return 0;
178
+ const clamped = Math.max(0, Math.min(renderIdx, offsets.length - 1));
179
+ return offsets[clamped];
180
+ }, [offsets]);
121
181
  const handleScrollTo = (0, _react.useCallback)((page, animated = true) => {
122
- if (getRef()) {
123
- getRef().scrollTo({
124
- x: offsets[page],
125
- y: 0,
126
- animated
127
- });
128
- }
129
- }, [getRef, offsets]);
130
- const jumpTo = (0, _react.useCallback)((page, delay = 200) => {
131
- expectedPosition.value = page;
182
+ (0, _reactNativeWorklets.scheduleOnUI)((pageIndex, isAnimated) => {
183
+ 'worklet';
132
184
 
133
- if (_reactNative.Platform.OS === 'android') {
134
- freeze.value = true;
135
- }
185
+ (0, _reactNativeReanimated.scrollTo)(internalAnimatedRef, offsets[pageIndex], 0, isAnimated);
186
+ }, page, animated);
187
+ }, [offsets, internalAnimatedRef]);
188
+
189
+ // ---- navigation ---------------------------------------------------------
136
190
 
137
- setTimeout(() => {
138
- animatedScroll.value = offsets[page];
139
- handleScrollTo(page, false);
140
- }, delay);
141
- }, [handleScrollTo, animatedScroll, freeze]);
142
191
  const goNext = (0, _react.useCallback)(() => {
143
- const next = currentPage.value + 1;
144
- handleScrollTo(next);
145
- }, [handleScrollTo]);
192
+ if (carouselState.value !== CarouselState.IDLE || data.length === 0) return;
193
+ const next = currentRenderPage.value + 1;
194
+ if (next < offsets.length) {
195
+ currentRenderPage.value = next;
196
+ handleScrollTo(next);
197
+ }
198
+ }, [handleScrollTo, offsets.length, data.length]);
146
199
  const goPrev = (0, _react.useCallback)(() => {
147
- const prev = currentPage.value - 1;
148
- handleScrollTo(prev);
200
+ if (carouselState.value !== CarouselState.IDLE || data.length === 0) return;
201
+ const prev = currentRenderPage.value - 1;
202
+ if (prev >= 0) {
203
+ currentRenderPage.value = prev;
204
+ handleScrollTo(prev);
205
+ }
149
206
  }, [handleScrollTo]);
150
207
  const snapToItem = (0, _react.useCallback)((index, animated = true) => {
208
+ if (data.length === 0) return;
151
209
  if (index < 0 || index >= data.length) {
152
210
  console.error(`Index not valid ${index}`);
153
211
  return;
154
212
  }
213
+ const renderIndex = loop ? index + additionalPagesPerSide : index;
214
+ const clamped = Math.max(0, Math.min(renderIndex, offsets.length - 1));
215
+ currentRenderPage.value = clamped;
216
+ currentLogicalPage.value = index;
217
+ handleScrollTo(clamped, animated);
218
+ }, [loop, additionalPagesPerSide, offsets.length, handleScrollTo, data.length]);
155
219
 
156
- let pageIndex = index;
220
+ // ---- context wiring -----------------------------------------------------
157
221
 
158
- if (loop) {
159
- const indices = Object.keys(pageMapper.current).filter(idx => pageMapper.current[Number(idx)] === index).map(idx => Number(idx));
160
- const toIndex = (0, _utils.findNearestPage)(currentPage.value, indices, 10);
161
- pageIndex = indices[toIndex];
162
- }
163
-
164
- handleScrollTo(pageIndex, animated);
165
- }, [handleScrollTo]);
166
222
  const {
167
223
  setCarouselHandlers
168
224
  } = (0, _useInternalCarouselContext.useInternalCarouselContext)();
@@ -175,128 +231,163 @@ function Carousel({
175
231
  });
176
232
  }
177
233
  }, [goNext, goPrev, snapToItem, setCarouselHandlers]);
178
- const handlePageChange = (0, _react.useCallback)(page => {
179
- const actualPage = getActualPage(page);
180
- animatedPage.value = actualPage;
181
-
182
- if (onPageChange) {
183
- onPageChange(actualPage);
184
- }
185
-
186
- if (!loop) return;
187
-
188
- if (page === pageItems.length - 1) {
189
- jumpTo(additionalPagesPerSide * 2 - 1);
190
- } else if (page === 0) {
191
- jumpTo(pageItems.length - additionalPagesPerSide * 2);
192
- }
193
- }, [onPageChange, loop, getActualPage, jumpTo, pageItems]);
194
- const refreshPage = (0, _react.useCallback)(offset => {
195
- 'worklet';
196
234
 
197
- const pageNum = (0, _utils.findNearestPage)(offset, offsets, 20);
235
+ // ---- autoplay -----------------------------------------------------------
198
236
 
199
- if (pageNum === -1) {
200
- return;
237
+ // isDragging acts as a debounce — autoplay resumes 200ms after the
238
+ // user lifts their finger, preventing an immediate auto-advance.
239
+ const [autoplayPaused, setAutoplayPaused] = (0, _react.useState)(false);
240
+ (0, _reactNativeHooks.useInterval)(() => {
241
+ // Only advance when carousel is idle (not dragging, not in a
242
+ // loop jump) and the app is in the foreground.
243
+ if (carouselState.value === CarouselState.IDLE && appActive.value) {
244
+ goNext();
201
245
  }
246
+ }, !autoPlay || !loop || autoplayPaused ? -1 : duration);
202
247
 
203
- if (pageNum !== currentPage.value) {
204
- if (expectedPosition.value === pageNum) {
205
- freeze.value = false;
206
- }
248
+ // ---- initial page setup -------------------------------------------------
207
249
 
208
- currentPage.value = pageNum;
209
- (0, _reactNativeReanimated.runOnJS)(handlePageChange)(pageNum);
210
- }
211
- }, [isDragging, offsets, handlePageChange]);
212
- (0, _reactNativeHooks.useInterval)(() => {
213
- goNext();
214
- }, !autoPlay || !loop || isDragging ? -1 : duration);
215
250
  (0, _react.useEffect)(() => {
251
+ if (data.length === 0) return;
216
252
  if (initialPage < 0 || initialPage >= data.length) {
217
253
  console.error(`Invalid initialPage ${initialPage}`);
218
254
  return;
219
255
  }
256
+ const renderIndex = loop ? initialPage + additionalPagesPerSide : initialPage;
257
+ const safeIdx = Math.max(0, Math.min(renderIndex, offsets.length - 1));
258
+ currentRenderPage.value = safeIdx;
259
+ currentLogicalPage.value = initialPage;
260
+ animatedPage.value = initialPage;
261
+ (0, _reactNativeWorklets.scheduleOnUI)(idx => {
262
+ 'worklet';
263
+
264
+ (0, _reactNativeReanimated.scrollTo)(internalAnimatedRef, offsets[idx], 0, false);
265
+ }, safeIdx);
266
+ animatedScroll.value = offsets[safeIdx] ?? 0;
267
+ // eslint-disable-next-line react-hooks/exhaustive-deps
268
+ }, []);
220
269
 
221
- let pageIndex = initialPage;
270
+ // ---- scroll handler (UI thread worklet) ---------------------------------
222
271
 
223
- if (loop) {
224
- pageIndex = initialPage + additionalPagesPerSide;
225
- }
272
+ const scrollHandler = (0, _reactNativeReanimated.useAnimatedScrollHandler)({
273
+ onScroll: event => {
274
+ animatedScroll.value = event.contentOffset.x;
226
275
 
227
- if (currentPage.value !== pageIndex) {
228
- setTimeout(() => {
229
- handleScrollTo(pageIndex, false);
276
+ // During a loop jump, detect when the scroll arrives at the
277
+ // destination and transition back to IDLE.
278
+ if (carouselState.value === CarouselState.LOOP_JUMP && jumpDestination.value !== -1) {
279
+ const nearest = (0, _utils.findNearestPage)(event.contentOffset.x, offsets);
280
+ if (nearest === jumpDestination.value) {
281
+ freeze.value = false;
282
+ carouselState.value = CarouselState.IDLE;
283
+ jumpDestination.value = -1;
284
+ }
285
+ }
286
+ },
287
+ onBeginDrag: _e => {
288
+ // User gesture always wins. Cancel any in-flight loop jump
289
+ // so the carousel doesn't fight the user's drag.
290
+ if (carouselState.value === CarouselState.LOOP_JUMP) {
291
+ jumpDestination.value = -1;
230
292
  freeze.value = false;
231
- });
232
- }
233
- }, []);
234
- const beginDrag = (0, _react.useCallback)(() => {
235
- if (autoPlay) {
236
- setDragging(true);
237
- }
238
- }, [autoPlay]);
239
- const endDrag = (0, _react.useCallback)(() => {
240
- if (autoPlay) {
241
- setTimeout(() => setDragging(false), 200);
293
+ }
294
+ carouselState.value = CarouselState.DRAGGING;
295
+ (0, _reactNativeWorklets.scheduleOnRN)(setAutoplayPaused, true);
296
+ },
297
+ onEndDrag: _e => {
298
+ // Debounce autoplay resume — let momentum settle first.
299
+ (0, _reactNativeWorklets.scheduleOnRN)(setAutoplayPaused, false);
300
+ },
301
+ onMomentumEnd: event => {
302
+ // Only process page changes when dragging ended naturally.
303
+ // Loop jumps are handled in onScroll.
304
+ if (carouselState.value !== CarouselState.DRAGGING) {
305
+ return;
306
+ }
307
+ const offset = event.contentOffset.x;
308
+ if (isNaN(offset) || offsets.length === 0) {
309
+ carouselState.value = CarouselState.IDLE;
310
+ return;
311
+ }
312
+ const renderPage = (0, _utils.findNearestPage)(offset, offsets);
313
+
314
+ // Loop boundary — user scrolled into the cloned head/tail pages.
315
+ if (loop) {
316
+ const boundaryTarget = (0, _utils.getLoopBoundaryTarget)(renderPage, data.length, additionalPagesPerSide);
317
+ if (boundaryTarget !== -1) {
318
+ // Jump instantly to the matching real page.
319
+ // On Android, freeze animations during the jump to prevent
320
+ // a visual flash from the clones.
321
+ carouselState.value = CarouselState.LOOP_JUMP;
322
+ jumpDestination.value = boundaryTarget;
323
+ freeze.value = _reactNative.Platform.OS === 'android';
324
+ animatedScroll.value = offsets[boundaryTarget];
325
+ (0, _reactNativeReanimated.scrollTo)(internalAnimatedRef, offsets[boundaryTarget], 0, false);
326
+ currentRenderPage.value = boundaryTarget;
327
+ currentLogicalPage.value = (0, _utils.getLogicalPage)(boundaryTarget, data.length, additionalPagesPerSide, loop);
328
+ (0, _reactNativeWorklets.scheduleOnRN)(handlePageChange, currentLogicalPage.value);
329
+ return;
330
+ }
331
+ }
332
+
333
+ // Normal page change — no loop boundary involved.
334
+ carouselState.value = CarouselState.IDLE;
335
+ currentRenderPage.value = renderPage;
336
+ currentLogicalPage.value = (0, _utils.getLogicalPage)(renderPage, data.length, additionalPagesPerSide, loop);
337
+ (0, _reactNativeWorklets.scheduleOnRN)(handlePageChange, currentLogicalPage.value);
242
338
  }
243
- }, [autoPlay]);
339
+ }, [offsets, loop, data.length, additionalPagesPerSide, handlePageChange, internalAnimatedRef]);
340
+
341
+ // ---- render helpers -----------------------------------------------------
342
+
244
343
  const getItemKey = (0, _react.useCallback)((item, index) => {
245
344
  if (keyExtractor) {
246
345
  return `${keyExtractor(item, index)}-${index}`;
247
346
  }
248
-
249
347
  if (item.id) {
250
348
  return `${item.id}-${index}`;
251
349
  }
252
-
253
- console.error('You need implement keyExtractor');
254
- return '';
350
+ return `carousel-item-${index}`;
255
351
  }, [keyExtractor]);
256
- const scrollHandler = (0, _reactNativeReanimated.useAnimatedScrollHandler)({
257
- onScroll: event => {
258
- animatedScroll.value = event.contentOffset.x;
259
- refreshPage(animatedScroll.value);
260
- },
261
- onBeginDrag: _e => {
262
- (0, _reactNativeReanimated.runOnJS)(beginDrag)();
263
- },
264
- onEndDrag: _e => {
265
- (0, _reactNativeReanimated.runOnJS)(endDrag)();
266
- }
267
- }, [beginDrag, endDrag, refreshPage]);
268
- const containerStyle = (0, _react.useCallback)(index => {
269
- if (firstItemAlignment === 'start') {
352
+ const containerStyles = (0, _react.useMemo)(() => {
353
+ if (data.length === 0) return [];
354
+ return pageItems.map((_, i) => {
355
+ if (firstItemAlignment !== 'start') {
356
+ return {
357
+ paddingLeft: spaceBetween / 2,
358
+ paddingRight: spaceBetween / 2
359
+ };
360
+ }
361
+ const logicalIndex = loop ? (0, _utils.getLogicalPage)(i, data.length, additionalPagesPerSide, loop) : i;
270
362
  return {
271
- paddingLeft: index === 0 ? 0 : spaceBetween / 2,
272
- paddingRight: index === data.length - 1 ? 0 : spaceBetween / 2
363
+ paddingLeft: logicalIndex === 0 ? 0 : spaceBetween / 2,
364
+ paddingRight: logicalIndex === data.length - 1 ? 0 : spaceBetween / 2
273
365
  };
274
- }
275
-
276
- return {
277
- paddingLeft: spaceBetween / 2,
278
- paddingRight: spaceBetween / 2
279
- };
280
- }, [spaceBetween, firstItemAlignment]);
281
- const handleItemPress = (0, _react.useCallback)((item, index) => () => {
282
- handleScrollTo(index);
283
-
284
- if (onItemPress) {
285
- onItemPress(item, index);
286
- }
287
- }, [onItemPress, handlePageChange]);
288
- const contentContainerStyle = (0, _react.useMemo)(() => {
289
- return {
290
- paddingHorizontal: horizontalPadding
291
- };
292
- }, [horizontalPadding]);
293
-
294
- function renderPage(item, i) {
295
- return /*#__PURE__*/_react.default.createElement(_PageItem.default, {
296
- key: getItemKey(item, i),
297
- containerStyle: containerStyle(i),
366
+ });
367
+ }, [pageItems, firstItemAlignment, spaceBetween, data.length, loop, additionalPagesPerSide]);
368
+ const itemPressHandlers = (0, _react.useMemo)(() => {
369
+ if (disableItemPress) return [];
370
+ return pageItems.map((item, i) => {
371
+ const logicalIndex = loop ? (0, _utils.getLogicalPage)(i, data.length, additionalPagesPerSide, loop) : i;
372
+ return () => {
373
+ handleScrollTo(i);
374
+ if (onItemPress) {
375
+ onItemPress(item, logicalIndex);
376
+ }
377
+ };
378
+ });
379
+ }, [pageItems, disableItemPress, loop, data.length, additionalPagesPerSide, handleScrollTo, onItemPress]);
380
+ const contentContainerStyle = (0, _react.useMemo)(() => ({
381
+ paddingHorizontal: horizontalPadding
382
+ }), [horizontalPadding]);
383
+ const renderPage = (0, _react.useCallback)((item, i) => {
384
+ // Compute the logical index so consumer callbacks (renderItem,
385
+ // onItemPress via `index` prop) receive logical, not render.
386
+ const logicalIndex = loop ? (0, _utils.getLogicalPage)(i, data.length, additionalPagesPerSide, loop) : i;
387
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_PageItem.default, {
388
+ containerStyle: containerStyles[i],
298
389
  item: item,
299
- index: i,
390
+ index: logicalIndex,
300
391
  offset: offsets[i],
301
392
  itemWidth: itemWidth,
302
393
  animatedValue: animatedScroll,
@@ -305,15 +396,21 @@ function Carousel({
305
396
  freeze: freeze,
306
397
  inactiveOpacity: inactiveOpacity,
307
398
  inactiveScale: inactiveScale,
308
- onPress: handleItemPress(item, i)
309
- });
310
- }
311
-
312
- return /*#__PURE__*/_react.default.createElement(_reactNative.View, {
313
- style: style
314
- }, /*#__PURE__*/_react.default.createElement(_reactNativeReanimated.default.ScrollView, _extends({}, scrollViewProps, {
315
- ref: scrollViewRef,
316
- style: styles.container,
399
+ onPress: itemPressHandlers[i]
400
+ }, getItemKey(item, i));
401
+ }, [loop, data.length, additionalPagesPerSide, getItemKey, containerStyles, offsets, itemWidth, animatedScroll, animation, renderItem, freeze, inactiveOpacity, inactiveScale, itemPressHandlers]);
402
+
403
+ // ---- render -------------------------------------------------------------
404
+
405
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.ScrollView, {
406
+ ...scrollViewProps,
407
+ ref: nativeRef => {
408
+ internalAnimatedRef(nativeRef);
409
+ if (externalScrollViewRef) {
410
+ externalScrollViewRef.current = nativeRef;
411
+ }
412
+ },
413
+ style: [styles.container, style],
317
414
  horizontal: true,
318
415
  disableScrollViewPanResponder: true,
319
416
  disableIntervalMomentum: true,
@@ -325,13 +422,11 @@ function Carousel({
325
422
  scrollEventThrottle: 4,
326
423
  onScroll: scrollHandler,
327
424
  bounces: false,
328
- contentContainerStyle: contentContainerStyle
329
- }), pageItems.map(renderPage)));
425
+ contentContainerStyle: contentContainerStyle,
426
+ children: pageItems.map(renderPage)
427
+ });
330
428
  }
331
-
332
- var _default = Carousel;
333
- exports.default = _default;
334
-
429
+ var _default = exports.default = Carousel;
335
430
  const styles = _reactNative.StyleSheet.create({
336
431
  container: {
337
432
  flex: 1