@coinbase/cds-mobile 8.38.7 → 8.39.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/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
8
8
 
9
9
  <!-- template-start -->
10
10
 
11
+ ## 8.39.0 (1/27/2026 PST)
12
+
13
+ #### 🚀 Updates
14
+
15
+ - Support Carousel looping. [[#327](https://github.com/coinbase/cds/pull/327)]
16
+
11
17
  ## 8.38.7 (1/26/2026 PST)
12
18
 
13
19
  #### 🐞 Fixes
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
  import { type StyleProp, type TextStyle, type ViewStyle } from 'react-native';
3
- import type { Rect, SharedAccessibilityProps, SharedProps } from '@coinbase/cds-common/types';
3
+ import type { SharedAccessibilityProps, SharedProps } from '@coinbase/cds-common/types';
4
4
  import { type BoxBaseProps, type BoxProps } from '../layout/Box';
5
+ import { CarouselContext, type CarouselContextValue, useCarouselContext } from './CarouselContext';
5
6
  export type CarouselItemRenderChildren = React.FC<{
6
7
  isVisible: boolean;
7
8
  }>;
@@ -20,16 +21,8 @@ export type CarouselItemBaseProps = Omit<BoxBaseProps, 'children'> &
20
21
  export type CarouselItemProps = Omit<BoxProps, 'children'> & CarouselItemBaseProps;
21
22
  export type CarouselItemComponent = React.FC<CarouselItemProps>;
22
23
  export type CarouselItemElement = React.ReactElement<CarouselItemProps, CarouselItemComponent>;
23
- export type CarouselContextValue = {
24
- registerItem: (id: string, state: Rect) => void;
25
- unregisterItem: (id: string) => void;
26
- /**
27
- * Set of item IDs that are currently visible in the carousel viewport.
28
- */
29
- visibleCarouselItems: Set<string>;
30
- };
31
- export declare const CarouselContext: React.Context<CarouselContextValue | undefined>;
32
- export declare const useCarouselContext: () => CarouselContextValue;
24
+ export { CarouselContext, useCarouselContext };
25
+ export type { CarouselContextValue };
33
26
  export type CarouselNavigationComponentBaseProps = {
34
27
  /**
35
28
  * Callback for when the previous button is pressed.
@@ -180,6 +173,12 @@ export type CarouselBaseProps = SharedProps &
180
173
  * Callback fired when the user ends dragging the carousel.
181
174
  */
182
175
  onDragEnd?: () => void;
176
+ /**
177
+ * Enables infinite looping. When true, the carousel will seamlessly
178
+ * loop from the last item back to the first.
179
+ * @note Requires at least 2 pages worth of content to function.
180
+ */
181
+ loop?: boolean;
183
182
  };
184
183
  export type CarouselProps = CarouselBaseProps & {
185
184
  /**
@@ -312,6 +311,12 @@ export declare const Carousel: React.MemoExoticComponent<
312
311
  * Callback fired when the user ends dragging the carousel.
313
312
  */
314
313
  onDragEnd?: () => void;
314
+ /**
315
+ * Enables infinite looping. When true, the carousel will seamlessly
316
+ * loop from the last item back to the first.
317
+ * @note Requires at least 2 pages worth of content to function.
318
+ */
319
+ loop?: boolean;
315
320
  } & {
316
321
  /**
317
322
  * Custom styles for the root element.
@@ -1 +1 @@
1
- {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/carousel/Carousel.tsx"],"names":[],"mappings":"AAAA,OAAO,KASN,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpF,OAAO,KAAK,EAAE,IAAI,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAI9F,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAQjE,MAAM,MAAM,0BAA0B,GAAG,KAAK,CAAC,EAAE,CAAC;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AAE1E,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,GAChE,wBAAwB,GAAG;IACzB;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IACX;;;OAGG;IACH,QAAQ,CAAC,EAAE,0BAA0B,GAAG,KAAK,CAAC,SAAS,CAAC;CACzD,CAAC;AAEJ,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,GAAG,qBAAqB,CAAC;AAEnF,MAAM,MAAM,qBAAqB,GAAG,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC;AAChE,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAC,YAAY,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,CAAC;AAE/F,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,KAAK,IAAI,CAAC;IAChD,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;OAEG;IACH,oBAAoB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,eAAO,MAAM,eAAe,iDAAmE,CAAC;AAEhG,eAAO,MAAM,kBAAkB,QAAO,oBAMrC,CAAC;AAEF,MAAM,MAAM,oCAAoC,GAAG;IACjD;;OAEG;IACH,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB;;OAEG;IACH,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB;;OAEG;IACH,iBAAiB,EAAE,OAAO,CAAC;IAC3B;;OAEG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB;;OAEG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC;;OAEG;IACH,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG,oCAAoC,GAAG;IACpF;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG,KAAK,CAAC,EAAE,CAAC,gCAAgC,CAAC,CAAC;AAErF,MAAM,MAAM,oCAAoC,GAAG;IACjD;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;OAEG;IACH,4BAA4B,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG,oCAAoC,GAAG;IACpF;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG,KAAK,CAAC,EAAE,CAAC,gCAAgC,CAAC,CAAC;AAErF,MAAM,MAAM,wBAAwB,GAAG;IACrC;;OAEG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,WAAW,GACzC,wBAAwB,GACxB,YAAY,GAAG;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAC;IACvD;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B;;OAEG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;OAEG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,2BAA2B,CAAC;IAClD;;;OAGG;IACH,mBAAmB,CAAC,EAAE,2BAA2B,CAAC;IAClD;;;;;OAKG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB;;OAEG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC;;OAEG;IACH,8BAA8B,CAAC,EAAE,MAAM,CAAC;IACxC;;OAEG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC;;OAEG;IACH,4BAA4B,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IACxE;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAEJ,MAAM,MAAM,aAAa,GAAG,iBAAiB,GAAG;IAC9C;;OAEG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;OAEG;IACH,MAAM,CAAC,EAAE;QACP;;WAEG;QACH,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAC5B;;WAEG;QACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAC7B;;WAEG;QACH,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAClC;;WAEG;QACH,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAClC;;WAEG;QACH,QAAQ,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAChC;;WAEG;QACH,iBAAiB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;KAC1C,CAAC;CACH,CAAC;AAyKF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;IAxRjB;;;OAGG;eACQ,mBAAmB,GAAG,mBAAmB,EAAE;IACtD;;;;;;;OAOG;WACI,MAAM,GAAG,MAAM,GAAG,MAAM;IAC/B;;;;;;OAMG;eACQ,MAAM,GAAG,MAAM;IAC1B;;OAEG;qBACc,OAAO;IACxB;;OAEG;qBACc,OAAO;IACxB;;;OAGG;0BACmB,2BAA2B;IACjD;;;OAGG;0BACmB,2BAA2B;IACjD;;;;;OAKG;YACK,KAAK,CAAC,SAAS;IACvB;;OAEG;iCAC0B,MAAM;IACnC;;OAEG;qCAC8B,MAAM;IACvC;;OAEG;iCAC0B,MAAM;IACnC;;OAEG;mCAC4B,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACvE;;OAEG;mBACY,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI;IAChD;;OAEG;kBACW,MAAM,IAAI;IACxB;;OAEG;gBACS,MAAM,IAAI;;IAIxB;;OAEG;YACK,SAAS,CAAC,SAAS,CAAC;IAC5B;;OAEG;aACM;QACP;;WAEG;QACH,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAC5B;;WAEG;QACH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAC7B;;WAEG;QACH,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAClC;;WAEG;QACH,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAClC;;WAEG;QACH,QAAQ,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAChC;;WAEG;QACH,iBAAiB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;KAC1C;mDA4fF,CAAC"}
1
+ {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/carousel/Carousel.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import type { Rect } from '@coinbase/cds-common/types';
3
+ export type CarouselContextValue = {
4
+ registerItem: (id: string, state: Rect) => void;
5
+ unregisterItem: (id: string) => void;
6
+ /**
7
+ * Set of item IDs that are currently visible in the carousel viewport.
8
+ */
9
+ visibleCarouselItems: Set<string>;
10
+ };
11
+ export declare const CarouselContext: React.Context<CarouselContextValue | undefined>;
12
+ export declare const useCarouselContext: () => CarouselContextValue;
13
+ //# sourceMappingURL=CarouselContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CarouselContext.d.ts","sourceRoot":"","sources":["../../src/carousel/CarouselContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,KAAK,IAAI,CAAC;IAChD,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;OAEG;IACH,oBAAoB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,eAAO,MAAM,eAAe,iDAAmE,CAAC;AAEhG,eAAO,MAAM,kBAAkB,QAAO,oBAMrC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { type CarouselItemProps } from './Carousel';
2
+ import type { CarouselItemProps } from './Carousel';
3
3
  export declare const CarouselItem: React.MemoExoticComponent<
4
4
  ({ children, id, style, ...props }: CarouselItemProps) => import('react/jsx-runtime').JSX.Element
5
5
  >;
@@ -1 +1 @@
1
- {"version":3,"file":"CarouselItem.d.ts","sourceRoot":"","sources":["../../src/carousel/CarouselItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAK5D,OAAO,EAAE,KAAK,iBAAiB,EAAsB,MAAM,YAAY,CAAC;AAExE,eAAO,MAAM,YAAY,gEAA4C,iBAAiB,6CAwCpF,CAAC"}
1
+ {"version":3,"file":"CarouselItem.d.ts","sourceRoot":"","sources":["../../src/carousel/CarouselItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAK5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGpD,eAAO,MAAM,YAAY,gEAA4C,iBAAiB,6CAwCpF,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export * from './Carousel';
2
+ export * from './CarouselContext';
2
3
  export * from './CarouselItem';
3
4
  export * from './DefaultCarouselNavigation';
4
5
  export * from './DefaultCarouselPagination';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/carousel/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/carousel/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC"}
@@ -1,7 +1,7 @@
1
- const _excluded = ["children", "title", "hideNavigation", "hidePagination", "drag", "snapMode", "NavigationComponent", "PaginationComponent", "style", "styles", "nextPageAccessibilityLabel", "previousPageAccessibilityLabel", "goToPageAccessibilityLabel", "onChangePage", "onDragStart", "onDragEnd"];
1
+ const _excluded = ["children", "title", "hideNavigation", "hidePagination", "drag", "snapMode", "NavigationComponent", "PaginationComponent", "style", "styles", "nextPageAccessibilityLabel", "previousPageAccessibilityLabel", "goToPageAccessibilityLabel", "onChangePage", "onDragStart", "onDragEnd", "loop"];
2
2
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
3
3
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
4
- import React, { forwardRef, memo, useCallback, useContext, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
+ import React, { forwardRef, memo, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
5
5
  import { View } from 'react-native';
6
6
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
7
7
  import { animated, useSpring } from '@react-spring/native';
@@ -9,24 +9,40 @@ import { useLayout } from '../hooks/useLayout';
9
9
  import { HStack } from '../layout/HStack';
10
10
  import { VStack } from '../layout/VStack';
11
11
  import { Text } from '../typography/Text';
12
+ import { CarouselContext, useCarouselContext } from './CarouselContext';
13
+ import { CarouselItem } from './CarouselItem';
12
14
  import { DefaultCarouselNavigation } from './DefaultCarouselNavigation';
13
15
  import { DefaultCarouselPagination } from './DefaultCarouselPagination';
16
+
17
+ /**
18
+ * Wraps a value within a range (min, max) for circular indexing.
19
+ * @param min - The minimum value of the range.
20
+ * @param max - The maximum value of the range (exclusive).
21
+ * @param value - The value to wrap.
22
+ * @returns The wrapped value within the range.
23
+ */
14
24
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
- export const CarouselContext = /*#__PURE__*/React.createContext(undefined);
16
- export const useCarouselContext = () => {
17
- const context = useContext(CarouselContext);
18
- if (!context) {
19
- throw new Error('useCarouselContext must be used within a Carousel component');
20
- }
21
- return context;
25
+ const wrap = (min, max, value) => {
26
+ const range = max - min;
27
+ return min + ((value - min) % range + range) % range;
22
28
  };
29
+ export { CarouselContext, useCarouselContext };
23
30
  /**
24
31
  * Calculates the locations of each item in the carousel, offset from the first item.
25
32
  * @param itemRects - The items to get the offsets for.
26
33
  * @returns The item offsets.
27
34
  */
28
35
  const getItemOffsets = itemRects => {
29
- const sortedItems = Object.values(itemRects).sort((a, b) => a.x - b.x);
36
+ // Filter out clone items (they have IDs starting with "clone-")
37
+ const originalItems = Object.entries(itemRects).filter(_ref => {
38
+ let [id] = _ref;
39
+ return !id.startsWith('clone-');
40
+ }).map(_ref2 => {
41
+ let [, rect] = _ref2;
42
+ return rect;
43
+ });
44
+ if (originalItems.length === 0) return [];
45
+ const sortedItems = originalItems.sort((a, b) => a.x - b.x);
30
46
  const initialItemOffset = sortedItems[0].x;
31
47
  return sortedItems.map(item => _extends({}, item, {
32
48
  x: item.x - initialItemOffset
@@ -52,14 +68,61 @@ const getNearestPageIndexFromOffset = (scrollOffset, pageOffsets) => {
52
68
  return closestPageIndex;
53
69
  };
54
70
 
71
+ /**
72
+ * Finds the nearest offset from a set of candidate offsets, considering loop cycles.
73
+ * Checks current, previous, and next cycles to find the shortest path.
74
+ * @param currentOffset - The current scroll offset.
75
+ * @param candidateOffsets - Array of candidate offsets within a single loop cycle.
76
+ * @param loopLength - The total length of one loop cycle.
77
+ * @returns The nearest offset and its index in the candidates array.
78
+ */
79
+ const findNearestLoopOffset = (currentOffset, candidateOffsets, loopLength) => {
80
+ const currentCycle = Math.floor(currentOffset / loopLength);
81
+ let nearest = {
82
+ offset: 0,
83
+ index: 0,
84
+ distance: Infinity
85
+ };
86
+ for (const [index, candidateOffset] of candidateOffsets.entries()) {
87
+ for (const cycle of [currentCycle - 1, currentCycle, currentCycle + 1]) {
88
+ const cycleOffset = cycle * loopLength + candidateOffset;
89
+ const distance = Math.abs(currentOffset - cycleOffset);
90
+ if (distance < nearest.distance) {
91
+ nearest = {
92
+ offset: cycleOffset,
93
+ index,
94
+ distance
95
+ };
96
+ }
97
+ }
98
+ }
99
+ return {
100
+ offset: nearest.offset,
101
+ index: nearest.index
102
+ };
103
+ };
104
+
55
105
  /**
56
106
  * Calculates the offsets for a given set of items grouped by item.
107
+ * @note when looping, all items have a page offset, otherwise we find
108
+ * the last item that can start a page and still show all remaining items.
57
109
  * @param items - The items to get the page offsets for.
58
110
  * @param containerWidth - The width of the container.
59
111
  * @param maxScrollOffset - The maximum scroll offset.
112
+ * @param loop - Whether looping is enabled.
60
113
  * @returns The page offsets and the total number of pages.
61
114
  */
62
- const getSnapItemPageOffsets = (items, containerWidth, maxScrollOffset) => {
115
+ const getSnapItemPageOffsets = (items, containerWidth, maxScrollOffset, loop) => {
116
+ if (loop) {
117
+ const offsets = [];
118
+ for (let i = 0; i < items.length; i++) {
119
+ offsets.push(items[i].x);
120
+ }
121
+ return {
122
+ totalPages: offsets.length,
123
+ pageOffsets: offsets
124
+ };
125
+ }
63
126
  let lastPageStartIndex = items.length - 1;
64
127
  const lastItem = items[lastPageStartIndex];
65
128
  const lastItemsEndPosition = lastItem.x + lastItem.width;
@@ -146,6 +209,24 @@ const clampWithElasticResistance = function (offset, maxScrollOffset, elasticAmo
146
209
  return offset;
147
210
  };
148
211
 
212
+ /**
213
+ * Calculates how many items need to be cloned for looping to fill the viewport.
214
+ * For backward clones, pass the items array reversed.
215
+ * @param items - The item rects sorted by position (or reversed for backward clones).
216
+ * @param containerWidth - The width of the container viewport.
217
+ * @returns The number of items to clone.
218
+ */
219
+ const getCloneCount = (items, containerWidth) => {
220
+ let widthSum = 0;
221
+ let count = 0;
222
+ for (const item of items) {
223
+ widthSum += item.width;
224
+ count++;
225
+ if (widthSum >= containerWidth) break;
226
+ }
227
+ return Math.max(1, count);
228
+ };
229
+
149
230
  /**
150
231
  * Calculates which items are visible in the carousel based on scroll offset and viewport.
151
232
  * @param itemRects - The items to get the visibility for.
@@ -157,8 +238,8 @@ const getVisibleItems = (itemRects, containerWidth, scrollOffset) => {
157
238
  const visibleItems = new Set();
158
239
  const viewportLeft = scrollOffset;
159
240
  const viewportRight = scrollOffset + containerWidth;
160
- Object.entries(itemRects).forEach(_ref => {
161
- let [itemId, rect] = _ref;
241
+ Object.entries(itemRects).forEach(_ref3 => {
242
+ let [itemId, rect] = _ref3;
162
243
  const itemLeft = rect.x;
163
244
  const itemRight = rect.x + rect.width;
164
245
  const isVisible = itemLeft < viewportRight && itemRight > viewportLeft;
@@ -172,7 +253,7 @@ const animationConfig = {
172
253
  tension: 200,
173
254
  friction: 25
174
255
  };
175
- export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) => {
256
+ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref4, ref) => {
176
257
  let {
177
258
  children,
178
259
  title,
@@ -189,9 +270,10 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
189
270
  goToPageAccessibilityLabel,
190
271
  onChangePage,
191
272
  onDragStart,
192
- onDragEnd
193
- } = _ref2,
194
- props = _objectWithoutPropertiesLoose(_ref2, _excluded);
273
+ onDragEnd,
274
+ loop
275
+ } = _ref4,
276
+ props = _objectWithoutPropertiesLoose(_ref4, _excluded);
195
277
  const carouselScrollX = useRef(0);
196
278
  const animationApi = useSpring({
197
279
  x: carouselScrollX.current,
@@ -218,15 +300,83 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
218
300
  const lastItem = items[items.length - 1];
219
301
  return lastItem.x + lastItem.width;
220
302
  }, [carouselItemRects]);
303
+ const maxScrollOffset = Math.max(0, contentWidth - containerSize.width);
304
+ const hasCalculatedDimensions = contentWidth > 0 && containerSize.width > 0;
305
+
306
+ // Calculate gap between items (needed for loopLength to maintain consistent spacing at wrap seam)
307
+ const gap = useMemo(() => {
308
+ if (Object.keys(carouselItemRects).length < 2) return 0;
309
+ const items = getItemOffsets(carouselItemRects);
310
+ const firstItemEnd = items[0].x + items[0].width;
311
+ const secondItemStart = items[1].x;
312
+ return Math.max(0, secondItemStart - firstItemEnd);
313
+ }, [carouselItemRects]);
314
+ const shouldLoop = useMemo(() => loop && hasCalculatedDimensions && maxScrollOffset > 0, [loop, hasCalculatedDimensions, maxScrollOffset]);
315
+ const loopLength = useMemo(() => {
316
+ if (!shouldLoop) return 0;
317
+ return contentWidth + gap;
318
+ }, [shouldLoop, contentWidth, gap]);
319
+ const isLoopingActive = shouldLoop && loopLength > 0;
320
+
321
+ // Calculate how many items to clone for each direction (enough to fill viewport)
322
+ const cloneCounts = useMemo(() => {
323
+ if (!shouldLoop || Object.keys(carouselItemRects).length === 0 || containerSize.width === 0) {
324
+ return {
325
+ forward: 0,
326
+ backward: 0
327
+ };
328
+ }
329
+ const items = getItemOffsets(carouselItemRects);
330
+ return {
331
+ forward: getCloneCount(items, containerSize.width),
332
+ backward: getCloneCount([...items].reverse(), containerSize.width)
333
+ };
334
+ }, [shouldLoop, carouselItemRects, containerSize.width]);
221
335
  const updateVisibleCarouselItems = useCallback(scrollOffset => {
222
336
  if (containerSize.width === 0) {
223
337
  setVisibleCarouselItems(new Set());
224
338
  return;
225
339
  }
226
- setVisibleCarouselItems(getVisibleItems(carouselItemRects, containerSize.width, scrollOffset));
227
- }, [carouselItemRects, containerSize.width]);
228
- const maxScrollOffset = Math.max(0, contentWidth - containerSize.width);
229
- const hasCalculatedDimensions = contentWidth > 0 && containerSize.width > 0;
340
+
341
+ // For original items: wrap the offset to check visibility within one cycle
342
+ const adjustedOffset = isLoopingActive ? (scrollOffset % loopLength + loopLength) % loopLength : scrollOffset;
343
+ const visibleItems = getVisibleItems(carouselItemRects, containerSize.width, adjustedOffset);
344
+
345
+ // For clones: check visibility against actual (unwrapped) scroll position
346
+ if (isLoopingActive && children) {
347
+ const childrenArray = React.Children.toArray(children);
348
+ const items = getItemOffsets(carouselItemRects);
349
+ const viewportLeft = scrollOffset;
350
+ const viewportRight = scrollOffset + containerSize.width;
351
+
352
+ // Check backward clones visibility
353
+ const backwardStartIndex = childrenArray.length - cloneCounts.backward;
354
+ for (let i = 0; i < cloneCounts.backward; i++) {
355
+ const originalIndex = backwardStartIndex + i;
356
+ const itemData = items[originalIndex];
357
+ if (itemData) {
358
+ const cloneX = itemData.x - loopLength;
359
+ const cloneRight = cloneX + itemData.width;
360
+ if (cloneX < viewportRight && cloneRight > viewportLeft) {
361
+ visibleItems.add("clone-backward-" + childrenArray[originalIndex].props.id);
362
+ }
363
+ }
364
+ }
365
+
366
+ // Check forward clones visibility
367
+ for (let i = 0; i < cloneCounts.forward; i++) {
368
+ const itemData = items[i];
369
+ if (itemData) {
370
+ const cloneX = itemData.x + loopLength;
371
+ const cloneRight = cloneX + itemData.width;
372
+ if (cloneX < viewportRight && cloneRight > viewportLeft) {
373
+ visibleItems.add("clone-forward-" + childrenArray[i].props.id);
374
+ }
375
+ }
376
+ }
377
+ }
378
+ setVisibleCarouselItems(visibleItems);
379
+ }, [carouselItemRects, containerSize.width, isLoopingActive, loopLength, children, cloneCounts]);
230
380
 
231
381
  // Calculate pages and their offsets based on snapMode
232
382
  const {
@@ -241,29 +391,37 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
241
391
  }
242
392
  let pageOffsets;
243
393
  if (snapMode === 'item') {
244
- pageOffsets = getSnapItemPageOffsets(getItemOffsets(carouselItemRects), containerSize.width, maxScrollOffset);
394
+ pageOffsets = getSnapItemPageOffsets(getItemOffsets(carouselItemRects), containerSize.width, maxScrollOffset, shouldLoop);
245
395
  } else {
246
396
  pageOffsets = getSnapPageOffsets(getItemOffsets(carouselItemRects), containerSize.width, maxScrollOffset);
247
397
  }
248
398
  updateActivePageIndex(pageIndex => Math.min(pageIndex, pageOffsets.totalPages - 1));
249
399
  return pageOffsets;
250
- }, [hasCalculatedDimensions, carouselItemRects, snapMode, containerSize.width, maxScrollOffset, updateActivePageIndex]);
400
+ }, [hasCalculatedDimensions, carouselItemRects, snapMode, containerSize.width, maxScrollOffset, shouldLoop, updateActivePageIndex]);
251
401
  const goToPage = useCallback(page => {
252
402
  const newPage = Math.max(0, Math.min(totalPages - 1, page));
253
403
  updateActivePageIndex(newPage);
254
- const targetOffset = pageOffsets[newPage];
404
+ updateVisibleCarouselItems(pageOffsets[newPage]);
405
+ const targetOffset = isLoopingActive ? findNearestLoopOffset(carouselScrollX.current, [pageOffsets[newPage]], loopLength).offset : pageOffsets[newPage];
255
406
  carouselScrollX.current = targetOffset;
256
- updateVisibleCarouselItems(targetOffset);
257
407
  animationApi.x.start({
258
408
  to: targetOffset,
259
409
  config: animationConfig
260
410
  });
261
- }, [totalPages, pageOffsets, updateVisibleCarouselItems, animationApi.x, updateActivePageIndex]);
411
+ }, [isLoopingActive, loopLength, totalPages, pageOffsets, animationApi.x, updateVisibleCarouselItems, updateActivePageIndex]);
262
412
  useImperativeHandle(ref, () => ({
263
413
  activePageIndex,
264
414
  totalPages,
265
415
  goToPage
266
416
  }), [activePageIndex, totalPages, goToPage]);
417
+ const handleGoNext = useCallback(() => {
418
+ const nextPage = shouldLoop ? wrap(0, totalPages, activePageIndex + 1) : activePageIndex + 1;
419
+ goToPage(nextPage);
420
+ }, [shouldLoop, totalPages, activePageIndex, goToPage]);
421
+ const handleGoPrevious = useCallback(() => {
422
+ const prevPage = shouldLoop ? wrap(0, totalPages, activePageIndex - 1) : activePageIndex - 1;
423
+ goToPage(prevPage);
424
+ }, [shouldLoop, totalPages, activePageIndex, goToPage]);
267
425
  const handleDragStart = useCallback(() => {
268
426
  onDragStart == null || onDragStart();
269
427
  }, [onDragStart]);
@@ -272,36 +430,68 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
272
430
  }, [onDragEnd]);
273
431
  const handleDragTransition = useCallback(targetOffsetScroll => {
274
432
  if (drag === 'none') return targetOffsetScroll;
275
- const closestPageIndex = getNearestPageIndexFromOffset(targetOffsetScroll, pageOffsets);
276
- updateActivePageIndex(closestPageIndex);
277
- if (drag === 'snap') {
278
- const snapOffset = pageOffsets[closestPageIndex];
279
- updateVisibleCarouselItems(snapOffset);
280
- return snapOffset;
433
+ if (isLoopingActive) {
434
+ const {
435
+ offset: nearestOffset,
436
+ index: pageIndex
437
+ } = findNearestLoopOffset(targetOffsetScroll, pageOffsets, loopLength);
438
+ updateActivePageIndex(pageIndex);
439
+ if (drag === 'snap') {
440
+ updateVisibleCarouselItems(pageOffsets[pageIndex]);
441
+ return nearestOffset;
442
+ }
443
+ const currentCycle = Math.floor(targetOffsetScroll / loopLength);
444
+ const localOffset = targetOffsetScroll - currentCycle * loopLength;
445
+ updateVisibleCarouselItems(localOffset);
446
+ return targetOffsetScroll;
447
+ } else {
448
+ // Non-looping logic with clamping
449
+ const clampedScrollOffset = clampWithElasticResistance(targetOffsetScroll, maxScrollOffset, 0);
450
+ const closestPageIndex = getNearestPageIndexFromOffset(clampedScrollOffset, pageOffsets);
451
+ updateActivePageIndex(closestPageIndex);
452
+ if (drag === 'snap') {
453
+ const snapOffset = pageOffsets[closestPageIndex];
454
+ updateVisibleCarouselItems(snapOffset);
455
+ return snapOffset;
456
+ }
457
+ updateVisibleCarouselItems(clampedScrollOffset);
458
+ return targetOffsetScroll;
281
459
  }
282
- updateVisibleCarouselItems(targetOffsetScroll);
283
- return targetOffsetScroll;
284
- }, [drag, pageOffsets, updateVisibleCarouselItems, updateActivePageIndex]);
460
+ }, [drag, isLoopingActive, loopLength, maxScrollOffset, pageOffsets, updateVisibleCarouselItems, updateActivePageIndex]);
285
461
  const panGesture = useMemo(() => Gesture.Pan().onStart(() => {
286
462
  if (!isDragEnabled) return;
287
463
  handleDragStart();
288
- }).onUpdate(_ref3 => {
464
+ }).onUpdate(_ref5 => {
289
465
  let {
290
466
  translationX
291
- } = _ref3;
467
+ } = _ref5;
292
468
  if (!isDragEnabled) return;
293
- const newOffset = clampWithElasticResistance(carouselScrollX.current - translationX, maxScrollOffset);
469
+ let newOffset;
470
+ if (shouldLoop) {
471
+ newOffset = carouselScrollX.current - translationX;
472
+ } else {
473
+ newOffset = clampWithElasticResistance(carouselScrollX.current - translationX, maxScrollOffset);
474
+ }
294
475
  animationApi.x.set(newOffset);
295
- }).onEnd(_ref4 => {
476
+ }).onEnd(_ref6 => {
296
477
  let {
297
478
  translationX,
298
479
  velocityX
299
- } = _ref4;
480
+ } = _ref6;
300
481
  if (!isDragEnabled) return;
301
- let projectedOffset = clampWithElasticResistance(carouselScrollX.current - translationX, maxScrollOffset);
482
+ let projectedOffset;
483
+ if (shouldLoop) {
484
+ projectedOffset = carouselScrollX.current - translationX;
485
+ } else {
486
+ projectedOffset = clampWithElasticResistance(carouselScrollX.current - translationX, maxScrollOffset);
487
+ }
302
488
  const power = drag === 'free' ? 0.25 : 0.125;
303
489
  const momentumDistance = velocityX * power;
304
- projectedOffset = clampWithElasticResistance(projectedOffset - momentumDistance, maxScrollOffset, 0);
490
+ if (shouldLoop) {
491
+ projectedOffset = projectedOffset - momentumDistance;
492
+ } else {
493
+ projectedOffset = clampWithElasticResistance(projectedOffset - momentumDistance, maxScrollOffset, 0);
494
+ }
305
495
  const finalOffset = handleDragTransition(projectedOffset);
306
496
  carouselScrollX.current = finalOffset;
307
497
  animationApi.x.start({
@@ -309,7 +499,59 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
309
499
  config: _extends({}, animationConfig)
310
500
  });
311
501
  handleDragEnd();
312
- }).runOnJS(true), [isDragEnabled, maxScrollOffset, animationApi, drag, handleDragTransition, handleDragStart, handleDragEnd]);
502
+ }).runOnJS(true), [isDragEnabled, shouldLoop, maxScrollOffset, animationApi, drag, handleDragTransition, handleDragStart, handleDragEnd]);
503
+ const childrenWithClones = useMemo(() => {
504
+ if (!loop) return children;
505
+ const childrenArray = React.Children.toArray(children);
506
+ if (childrenArray.length === 0) return children;
507
+ const result = [];
508
+
509
+ // Add backward clones (only when we have enough data to position them)
510
+ if (isLoopingActive && cloneCounts.backward > 0) {
511
+ const items = getItemOffsets(carouselItemRects);
512
+ const itemsToCloneBackward = childrenArray.slice(-cloneCounts.backward);
513
+ itemsToCloneBackward.forEach((child, cloneIndex) => {
514
+ var _itemData$x;
515
+ const originalIndex = childrenArray.length - cloneCounts.backward + cloneIndex;
516
+ const itemData = items[originalIndex];
517
+ const cloneId = "clone-backward-" + child.props.id;
518
+ result.push(/*#__PURE__*/_jsx(CarouselItem, {
519
+ "aria-hidden": true,
520
+ id: cloneId,
521
+ style: {
522
+ position: 'absolute',
523
+ left: ((_itemData$x = itemData == null ? void 0 : itemData.x) != null ? _itemData$x : 0) - loopLength,
524
+ width: itemData == null ? void 0 : itemData.width,
525
+ height: itemData == null ? void 0 : itemData.height
526
+ },
527
+ children: child.props.children
528
+ }, cloneId));
529
+ });
530
+ }
531
+
532
+ // Add original children (always present, never changes structure)
533
+ result.push(...childrenArray);
534
+
535
+ // Add forward clones (only when we have enough data)
536
+ if (isLoopingActive && cloneCounts.forward > 0) {
537
+ const items = getItemOffsets(carouselItemRects);
538
+ const itemsToCloneForward = childrenArray.slice(0, cloneCounts.forward);
539
+ itemsToCloneForward.forEach((child, cloneIndex) => {
540
+ const itemData = items[cloneIndex];
541
+ const cloneId = "clone-forward-" + child.props.id;
542
+ result.push(/*#__PURE__*/_jsx(CarouselItem, {
543
+ "aria-hidden": true,
544
+ id: cloneId,
545
+ style: {
546
+ width: itemData == null ? void 0 : itemData.width,
547
+ height: itemData == null ? void 0 : itemData.height
548
+ },
549
+ children: child.props.children
550
+ }, cloneId));
551
+ });
552
+ }
553
+ return result;
554
+ }, [loop, children, isLoopingActive, loopLength, cloneCounts, carouselItemRects]);
313
555
  const containerStyle = useMemo(() => [{
314
556
  flex: 1,
315
557
  overflow: 'hidden'
@@ -322,9 +564,15 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
322
564
  }, styles == null ? void 0 : styles.carousel), [styles == null ? void 0 : styles.carousel]);
323
565
  const animatedTransform = useMemo(() => ({
324
566
  transform: [{
325
- translateX: animationApi.x.to(value => -value)
567
+ translateX: animationApi.x.to(value => {
568
+ if (!shouldLoop || !loopLength) return -value;
569
+ // Wrap the value to stay within one cycle for visual continuity
570
+ // Ensure wrapped is always in range [0, loopLength)
571
+ const wrapped = (value % loopLength + loopLength) % loopLength;
572
+ return -wrapped;
573
+ })
326
574
  }]
327
- }), [animationApi]);
575
+ }), [animationApi, shouldLoop, loopLength]);
328
576
  const registerItem = useCallback((id, rect) => {
329
577
  setCarouselItemRects(prev => _extends({}, prev, {
330
578
  [id]: rect
@@ -361,11 +609,11 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
361
609
  style: styles == null ? void 0 : styles.title,
362
610
  children: title
363
611
  }) : title, !hideNavigation && /*#__PURE__*/_jsx(NavigationComponent, {
364
- disableGoNext: activePageIndex >= totalPages - 1,
365
- disableGoPrevious: activePageIndex <= 0,
612
+ disableGoNext: totalPages <= 1 || !shouldLoop && activePageIndex >= totalPages - 1,
613
+ disableGoPrevious: totalPages <= 1 || !shouldLoop && activePageIndex <= 0,
366
614
  nextPageAccessibilityLabel: nextPageAccessibilityLabel,
367
- onGoNext: () => goToPage(activePageIndex + 1),
368
- onGoPrevious: () => goToPage(activePageIndex - 1),
615
+ onGoNext: handleGoNext,
616
+ onGoPrevious: handleGoPrevious,
369
617
  previousPageAccessibilityLabel: previousPageAccessibilityLabel,
370
618
  style: styles == null ? void 0 : styles.navigation
371
619
  })]
@@ -376,7 +624,7 @@ export const Carousel = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref2, ref) =
376
624
  style: scrollViewStyle,
377
625
  children: /*#__PURE__*/_jsx(animated.View, {
378
626
  style: [animatedStyle, animatedTransform],
379
- children: children
627
+ children: childrenWithClones
380
628
  })
381
629
  })
382
630
  }), !hidePagination && /*#__PURE__*/_jsx(PaginationComponent, {
@@ -0,0 +1,9 @@
1
+ import React, { useContext } from 'react';
2
+ export const CarouselContext = /*#__PURE__*/React.createContext(undefined);
3
+ export const useCarouselContext = () => {
4
+ const context = useContext(CarouselContext);
5
+ if (!context) {
6
+ throw new Error('useCarouselContext must be used within a Carousel component');
7
+ }
8
+ return context;
9
+ };
@@ -4,7 +4,7 @@ function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t =
4
4
  import React, { memo, useCallback, useEffect } from 'react';
5
5
  import { useLayout } from '../hooks/useLayout';
6
6
  import { Box } from '../layout/Box';
7
- import { useCarouselContext } from './Carousel';
7
+ import { useCarouselContext } from './CarouselContext';
8
8
  import { jsx as _jsx } from "react/jsx-runtime";
9
9
  export const CarouselItem = /*#__PURE__*/memo(_ref => {
10
10
  let {
@@ -616,6 +616,80 @@ const ImperativeApiExample = () => {
616
616
  })
617
617
  });
618
618
  };
619
+ const LoopingExamples = () => {
620
+ const theme = useTheme();
621
+ const horizontalPadding = theme.space[2];
622
+ const windowWidth = Dimensions.get('window').width;
623
+ const carouselSizing = windowWidth - horizontalPadding * 2;
624
+ const horizontalGap = theme.space[1];
625
+ const threeItemsWidth = (carouselSizing - horizontalGap * 2) / 3;
626
+ return /*#__PURE__*/_jsxs(_Fragment, {
627
+ children: [/*#__PURE__*/_jsx(Example, {
628
+ paddingX: 0,
629
+ children: /*#__PURE__*/_jsx(Carousel, {
630
+ loop: true,
631
+ drag: "snap",
632
+ snapMode: "page",
633
+ styles: {
634
+ root: {
635
+ paddingHorizontal: horizontalPadding
636
+ },
637
+ carousel: {
638
+ gap: horizontalGap
639
+ }
640
+ },
641
+ title: "Looping - Snap Page",
642
+ children: sampleItems.map((item, index) => /*#__PURE__*/_jsx(CarouselItem, {
643
+ id: "loop-page-" + index,
644
+ width: threeItemsWidth,
645
+ children: item
646
+ }, "loop-page-" + index))
647
+ })
648
+ }), /*#__PURE__*/_jsx(Example, {
649
+ paddingX: 0,
650
+ children: /*#__PURE__*/_jsx(Carousel, {
651
+ loop: true,
652
+ drag: "snap",
653
+ snapMode: "item",
654
+ styles: {
655
+ root: {
656
+ paddingHorizontal: horizontalPadding
657
+ },
658
+ carousel: {
659
+ gap: horizontalGap
660
+ }
661
+ },
662
+ title: "Looping - Snap Item",
663
+ children: sampleItems.map((item, index) => /*#__PURE__*/_jsx(CarouselItem, {
664
+ id: "loop-item-" + index,
665
+ width: threeItemsWidth,
666
+ children: item
667
+ }, "loop-item-" + index))
668
+ })
669
+ }), /*#__PURE__*/_jsx(Example, {
670
+ paddingX: 0,
671
+ children: /*#__PURE__*/_jsx(Carousel, {
672
+ loop: true,
673
+ drag: "free",
674
+ snapMode: "item",
675
+ styles: {
676
+ root: {
677
+ paddingHorizontal: horizontalPadding
678
+ },
679
+ carousel: {
680
+ gap: horizontalGap
681
+ }
682
+ },
683
+ title: "Looping - Free Drag",
684
+ children: sampleItems.map((item, index) => /*#__PURE__*/_jsx(CarouselItem, {
685
+ id: "loop-free-" + index,
686
+ width: threeItemsWidth,
687
+ children: item
688
+ }, "loop-free-" + index))
689
+ })
690
+ })]
691
+ });
692
+ };
619
693
  const AnimatedPaginationExample = () => {
620
694
  const theme = useTheme();
621
695
  const AnimatedDot = /*#__PURE__*/memo(_ref5 => {
@@ -736,6 +810,7 @@ const AnimatedPaginationExample = () => {
736
810
  };
737
811
  export default function CarouselScreen() {
738
812
  return /*#__PURE__*/_jsxs(ExampleScreen, {
739
- children: [/*#__PURE__*/_jsx(BasicExamples, {}), /*#__PURE__*/_jsx(CustomComponentsExample, {}), /*#__PURE__*/_jsx(DynamicContentExample, {}), /*#__PURE__*/_jsx(AnimatedExample, {}), /*#__PURE__*/_jsx(ImperativeApiExample, {}), /*#__PURE__*/_jsx(AnimatedPaginationExample, {})]
813
+ paddingX: 0,
814
+ children: [/*#__PURE__*/_jsx(BasicExamples, {}), /*#__PURE__*/_jsx(CustomComponentsExample, {}), /*#__PURE__*/_jsx(DynamicContentExample, {}), /*#__PURE__*/_jsx(AnimatedExample, {}), /*#__PURE__*/_jsx(ImperativeApiExample, {}), /*#__PURE__*/_jsx(AnimatedPaginationExample, {}), /*#__PURE__*/_jsx(LoopingExamples, {})]
740
815
  });
741
816
  }
@@ -1,4 +1,5 @@
1
1
  export * from './Carousel';
2
+ export * from './CarouselContext';
2
3
  export * from './CarouselItem';
3
4
  export * from './DefaultCarouselNavigation';
4
5
  export * from './DefaultCarouselPagination';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/cds-mobile",
3
- "version": "8.38.7",
3
+ "version": "8.39.0",
4
4
  "description": "Coinbase Design System - Mobile",
5
5
  "repository": {
6
6
  "type": "git",
@@ -198,7 +198,7 @@
198
198
  "react-native-svg": "^14.1.0"
199
199
  },
200
200
  "dependencies": {
201
- "@coinbase/cds-common": "^8.38.7",
201
+ "@coinbase/cds-common": "^8.39.0",
202
202
  "@coinbase/cds-icons": "^5.9.0",
203
203
  "@coinbase/cds-illustrations": "^4.29.0",
204
204
  "@coinbase/cds-lottie-files": "^3.3.4",