@wireservers-ui/react-natives 2.0.1 → 2.0.2

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 (404) hide show
  1. package/CHANGELOG.md +94 -71
  2. package/LICENSE +21 -21
  3. package/README.md +653 -613
  4. package/bin/cli.js +83 -5
  5. package/bin/init.js +470 -324
  6. package/package.json +1 -1
  7. package/src/accordion/accordion-content.tsx +30 -30
  8. package/src/accordion/accordion-icon.tsx +54 -54
  9. package/src/accordion/accordion-item.tsx +37 -37
  10. package/src/accordion/accordion-title-text.tsx +24 -24
  11. package/src/accordion/accordion-trigger.tsx +38 -38
  12. package/src/accordion/accordion.tsx +91 -91
  13. package/src/accordion/index.ts +24 -24
  14. package/src/accordion/styles.ts +74 -74
  15. package/src/accordion/types.ts +56 -56
  16. package/src/actionsheet/actionsheet-backdrop.tsx +23 -23
  17. package/src/actionsheet/actionsheet-content.tsx +19 -19
  18. package/src/actionsheet/actionsheet-drag-indicator-wrapper.tsx +19 -19
  19. package/src/actionsheet/actionsheet-drag-indicator.tsx +19 -19
  20. package/src/actionsheet/actionsheet-item-text.tsx +19 -19
  21. package/src/actionsheet/actionsheet-item.tsx +20 -20
  22. package/src/actionsheet/actionsheet-scroll-view.tsx +12 -12
  23. package/src/actionsheet/actionsheet.tsx +45 -45
  24. package/src/actionsheet/index.ts +20 -20
  25. package/src/actionsheet/styles.ts +25 -25
  26. package/src/actionsheet/types.ts +49 -49
  27. package/src/alert/alert-body.tsx +19 -19
  28. package/src/alert/alert-close-button.tsx +23 -23
  29. package/src/alert/alert-icon.tsx +40 -40
  30. package/src/alert/alert-text.tsx +22 -22
  31. package/src/alert/alert.tsx +33 -33
  32. package/src/alert/index.ts +15 -15
  33. package/src/alert/styles.ts +112 -112
  34. package/src/alert/types.ts +36 -36
  35. package/src/alert-dialog/alert-dialog.tsx +54 -54
  36. package/src/alert-dialog/index.ts +2 -2
  37. package/src/alert-dialog/styles.ts +40 -40
  38. package/src/alert-dialog/types.ts +40 -40
  39. package/src/aspect-ratio/aspect-ratio.tsx +20 -20
  40. package/src/aspect-ratio/index.ts +2 -2
  41. package/src/aspect-ratio/styles.ts +6 -6
  42. package/src/aspect-ratio/types.ts +7 -7
  43. package/src/avatar/avatar-badge.tsx +22 -22
  44. package/src/avatar/avatar-fallback-text.tsx +33 -33
  45. package/src/avatar/avatar-group.tsx +53 -53
  46. package/src/avatar/avatar-image.tsx +21 -21
  47. package/src/avatar/avatar.tsx +27 -27
  48. package/src/avatar/index.ts +14 -14
  49. package/src/avatar/styles.ts +94 -94
  50. package/src/avatar/types.ts +35 -35
  51. package/src/badge/badge-icon.tsx +20 -20
  52. package/src/badge/badge-text.tsx +24 -24
  53. package/src/badge/badge.tsx +39 -39
  54. package/src/badge/index.ts +11 -11
  55. package/src/badge/styles.ts +175 -175
  56. package/src/badge/types.ts +37 -37
  57. package/src/blockquote/blockquote.tsx +21 -21
  58. package/src/blockquote/index.ts +2 -2
  59. package/src/blockquote/styles.ts +11 -11
  60. package/src/blockquote/types.ts +6 -6
  61. package/src/box/box.tsx +19 -19
  62. package/src/box/index.ts +2 -2
  63. package/src/box/styles.ts +6 -6
  64. package/src/box/types.ts +6 -6
  65. package/src/breadcrumb/breadcrumb-item.tsx +20 -20
  66. package/src/breadcrumb/breadcrumb-link.tsx +20 -20
  67. package/src/breadcrumb/breadcrumb-text.tsx +19 -19
  68. package/src/breadcrumb/breadcrumb.tsx +43 -43
  69. package/src/breadcrumb/index.ts +12 -12
  70. package/src/breadcrumb/styles.ts +36 -36
  71. package/src/breadcrumb/types.ts +33 -33
  72. package/src/button/button-group.tsx +35 -35
  73. package/src/button/button-icon.tsx +37 -37
  74. package/src/button/button-spinner.tsx +12 -12
  75. package/src/button/button-text.tsx +27 -27
  76. package/src/button/button.tsx +42 -42
  77. package/src/button/index.ts +19 -19
  78. package/src/button/styles.ts +250 -250
  79. package/src/button/types.ts +67 -67
  80. package/src/calendar/calendar-day-cell.tsx +67 -67
  81. package/src/calendar/calendar-day-view.tsx +66 -66
  82. package/src/calendar/calendar-event.tsx +59 -59
  83. package/src/calendar/calendar-header.tsx +60 -60
  84. package/src/calendar/calendar-horizontal-view.tsx +372 -372
  85. package/src/calendar/calendar-legend.tsx +41 -41
  86. package/src/calendar/calendar-month-view.tsx +47 -47
  87. package/src/calendar/calendar-vertical-view.tsx +395 -395
  88. package/src/calendar/calendar-view-switcher.tsx +65 -65
  89. package/src/calendar/calendar-week-view.tsx +52 -52
  90. package/src/calendar/calendar.tsx +74 -74
  91. package/src/calendar/index.ts +27 -27
  92. package/src/calendar/styles.ts +367 -367
  93. package/src/calendar/types.ts +101 -101
  94. package/src/calendar/use-calendar.ts +170 -170
  95. package/src/calendar/utils.ts +278 -278
  96. package/src/card/card-body.tsx +22 -22
  97. package/src/card/card-footer.tsx +19 -19
  98. package/src/card/card-header.tsx +22 -22
  99. package/src/card/card.tsx +27 -27
  100. package/src/card/index.ts +13 -13
  101. package/src/card/styles.ts +54 -54
  102. package/src/card/types.ts +31 -31
  103. package/src/carousel/carousel.tsx +436 -436
  104. package/src/carousel/index.ts +2 -2
  105. package/src/carousel/styles.ts +21 -21
  106. package/src/carousel/types.ts +41 -41
  107. package/src/center/center.tsx +19 -19
  108. package/src/center/index.ts +2 -2
  109. package/src/center/styles.ts +6 -6
  110. package/src/center/types.ts +6 -6
  111. package/src/checkbox/checkbox-group.tsx +63 -63
  112. package/src/checkbox/checkbox-icon.tsx +35 -35
  113. package/src/checkbox/checkbox-indicator.tsx +30 -30
  114. package/src/checkbox/checkbox-label.tsx +24 -24
  115. package/src/checkbox/checkbox.tsx +86 -86
  116. package/src/checkbox/index.ts +14 -14
  117. package/src/checkbox/styles.ts +69 -69
  118. package/src/checkbox/types.ts +55 -55
  119. package/src/circular-progress/circular-progress.tsx +82 -82
  120. package/src/circular-progress/index.ts +2 -2
  121. package/src/circular-progress/styles.ts +31 -31
  122. package/src/circular-progress/types.ts +18 -18
  123. package/src/code/code.tsx +36 -36
  124. package/src/code/index.ts +2 -2
  125. package/src/code/styles.ts +25 -25
  126. package/src/code/types.ts +13 -13
  127. package/src/collapsible/collapsible.tsx +58 -58
  128. package/src/collapsible/index.ts +2 -2
  129. package/src/collapsible/styles.ts +5 -5
  130. package/src/collapsible/types.ts +21 -21
  131. package/src/color-picker/color-picker-box.tsx +115 -115
  132. package/src/color-picker/color-picker-slider.tsx +98 -98
  133. package/src/color-picker/color-picker.tsx +162 -162
  134. package/src/color-picker/color-utils.ts +215 -215
  135. package/src/color-picker/index.ts +34 -34
  136. package/src/color-picker/styles.ts +32 -32
  137. package/src/color-picker/types.ts +49 -49
  138. package/src/color-picker/use-pointer-drag.ts +80 -80
  139. package/src/container/container.tsx +19 -19
  140. package/src/container/index.ts +2 -2
  141. package/src/container/styles.ts +21 -21
  142. package/src/container/types.ts +10 -10
  143. package/src/date-picker/date-picker.tsx +136 -136
  144. package/src/date-picker/index.ts +15 -15
  145. package/src/date-picker/styles.ts +18 -18
  146. package/src/date-picker/types.ts +33 -33
  147. package/src/divider/divider.tsx +21 -21
  148. package/src/divider/index.ts +2 -2
  149. package/src/divider/styles.ts +14 -14
  150. package/src/divider/types.ts +7 -7
  151. package/src/drawer/drawer-backdrop.tsx +23 -23
  152. package/src/drawer/drawer-body.tsx +19 -19
  153. package/src/drawer/drawer-close-button.tsx +29 -29
  154. package/src/drawer/drawer-content.tsx +142 -142
  155. package/src/drawer/drawer-footer.tsx +19 -19
  156. package/src/drawer/drawer-header.tsx +19 -19
  157. package/src/drawer/drawer.tsx +54 -54
  158. package/src/drawer/index.ts +22 -22
  159. package/src/drawer/styles.ts +36 -36
  160. package/src/drawer/types.ts +62 -62
  161. package/src/empty/empty.tsx +53 -53
  162. package/src/empty/index.ts +2 -2
  163. package/src/empty/styles.ts +26 -26
  164. package/src/empty/types.ts +22 -22
  165. package/src/fab/fab-icon.tsx +20 -20
  166. package/src/fab/fab-label.tsx +22 -22
  167. package/src/fab/fab.tsx +45 -45
  168. package/src/fab/index.ts +11 -11
  169. package/src/fab/styles.ts +57 -57
  170. package/src/fab/types.ts +33 -33
  171. package/src/form-control/form-control-error-icon.tsx +25 -25
  172. package/src/form-control/form-control-error-message.tsx +40 -40
  173. package/src/form-control/form-control-helper-text.tsx +25 -25
  174. package/src/form-control/form-control-label-text.tsx +25 -25
  175. package/src/form-control/form-control-label.tsx +36 -36
  176. package/src/form-control/form-control.tsx +46 -46
  177. package/src/form-control/index.ts +20 -20
  178. package/src/form-control/styles.ts +105 -105
  179. package/src/form-control/types.ts +45 -45
  180. package/src/heading/heading.tsx +21 -21
  181. package/src/heading/index.ts +2 -2
  182. package/src/heading/styles.ts +24 -24
  183. package/src/heading/types.ts +19 -19
  184. package/src/icon/icon.tsx +21 -21
  185. package/src/icon/index.ts +2 -2
  186. package/src/icon/styles.ts +18 -18
  187. package/src/icon/types.ts +8 -8
  188. package/src/icon-button/icon-button.tsx +23 -23
  189. package/src/icon-button/index.ts +2 -2
  190. package/src/icon-button/styles.ts +78 -78
  191. package/src/icon-button/types.ts +15 -15
  192. package/src/image/image.tsx +20 -20
  193. package/src/image/index.ts +2 -2
  194. package/src/image/styles.ts +28 -28
  195. package/src/image/types.ts +11 -11
  196. package/src/index.ts +1039 -1039
  197. package/src/input/index.ts +13 -13
  198. package/src/input/input-field.tsx +35 -35
  199. package/src/input/input-icon.tsx +25 -25
  200. package/src/input/input-slot.tsx +24 -24
  201. package/src/input/input.tsx +73 -73
  202. package/src/input/styles.ts +90 -90
  203. package/src/input/types.ts +39 -39
  204. package/src/kbd/index.ts +2 -2
  205. package/src/kbd/kbd.tsx +21 -21
  206. package/src/kbd/styles.ts +11 -11
  207. package/src/kbd/types.ts +7 -7
  208. package/src/link/index.ts +4 -4
  209. package/src/link/link-text.tsx +19 -19
  210. package/src/link/link.tsx +31 -31
  211. package/src/link/styles.ts +19 -19
  212. package/src/link/types.ts +13 -13
  213. package/src/list/index.ts +2 -2
  214. package/src/list/list.tsx +55 -55
  215. package/src/list/styles.ts +8 -8
  216. package/src/list/types.ts +17 -17
  217. package/src/menu/index.ts +2 -2
  218. package/src/menu/menu.tsx +99 -99
  219. package/src/menu/styles.ts +14 -14
  220. package/src/menu/types.ts +30 -30
  221. package/src/modal/index.ts +18 -18
  222. package/src/modal/modal-backdrop.tsx +23 -23
  223. package/src/modal/modal-body.tsx +19 -19
  224. package/src/modal/modal-close-button.tsx +29 -29
  225. package/src/modal/modal-content.tsx +22 -22
  226. package/src/modal/modal-footer.tsx +19 -19
  227. package/src/modal/modal-header.tsx +19 -19
  228. package/src/modal/modal.tsx +50 -50
  229. package/src/modal/styles.ts +37 -37
  230. package/src/modal/types.ts +49 -49
  231. package/src/nativewind-env.d.ts +1 -1
  232. package/src/number-input/index.ts +18 -18
  233. package/src/number-input/number-input.tsx +161 -161
  234. package/src/number-input/styles.ts +35 -35
  235. package/src/number-input/types.ts +44 -44
  236. package/src/overlay/index.ts +2 -2
  237. package/src/overlay/overlay.tsx +21 -21
  238. package/src/overlay/styles.ts +6 -6
  239. package/src/overlay/types.ts +7 -7
  240. package/src/pagination/index.ts +2 -2
  241. package/src/pagination/pagination.tsx +58 -58
  242. package/src/pagination/styles.ts +27 -27
  243. package/src/pagination/types.ts +19 -19
  244. package/src/password-input/index.ts +14 -14
  245. package/src/password-input/password-input.tsx +79 -79
  246. package/src/password-input/styles.ts +25 -25
  247. package/src/password-input/types.ts +24 -24
  248. package/src/pin-input/index.ts +12 -12
  249. package/src/pin-input/pin-input.tsx +96 -96
  250. package/src/pin-input/styles.ts +16 -16
  251. package/src/pin-input/types.ts +26 -26
  252. package/src/popover/index.ts +2 -2
  253. package/src/popover/popover.tsx +98 -98
  254. package/src/popover/styles.ts +31 -31
  255. package/src/popover/types.ts +46 -46
  256. package/src/portal/index.ts +2 -2
  257. package/src/portal/portal.tsx +8 -8
  258. package/src/portal/styles.ts +2 -2
  259. package/src/portal/types.ts +3 -3
  260. package/src/pressable/index.ts +2 -2
  261. package/src/pressable/pressable.tsx +20 -20
  262. package/src/pressable/styles.ts +10 -10
  263. package/src/pressable/types.ts +6 -6
  264. package/src/progress/index.ts +9 -9
  265. package/src/progress/progress-filled-track.tsx +26 -26
  266. package/src/progress/progress.tsx +52 -52
  267. package/src/progress/styles.ts +34 -34
  268. package/src/progress/types.ts +28 -28
  269. package/src/radio/index.ts +14 -14
  270. package/src/radio/radio-group.tsx +61 -61
  271. package/src/radio/radio-icon.tsx +24 -24
  272. package/src/radio/radio-indicator.tsx +30 -30
  273. package/src/radio/radio-label.tsx +24 -24
  274. package/src/radio/radio.tsx +68 -68
  275. package/src/radio/styles.ts +69 -69
  276. package/src/radio/types.ts +51 -51
  277. package/src/rating/index.ts +7 -7
  278. package/src/rating/rating.tsx +93 -93
  279. package/src/rating/styles.ts +13 -13
  280. package/src/rating/types.ts +29 -29
  281. package/src/search-input/index.ts +16 -16
  282. package/src/search-input/search-input.tsx +119 -119
  283. package/src/search-input/styles.ts +28 -28
  284. package/src/search-input/types.ts +31 -31
  285. package/src/segmented-control/index.ts +2 -2
  286. package/src/segmented-control/segmented-control.tsx +34 -34
  287. package/src/segmented-control/styles.ts +22 -22
  288. package/src/segmented-control/types.ts +22 -22
  289. package/src/select/index.ts +28 -28
  290. package/src/select/select-backdrop.tsx +25 -25
  291. package/src/select/select-content.tsx +49 -49
  292. package/src/select/select-drag-indicator.tsx +19 -19
  293. package/src/select/select-icon.tsx +25 -25
  294. package/src/select/select-input.tsx +32 -32
  295. package/src/select/select-item-text.tsx +30 -30
  296. package/src/select/select-item.tsx +72 -72
  297. package/src/select/select-portal.tsx +22 -22
  298. package/src/select/select-scroll-view.tsx +22 -22
  299. package/src/select/select-trigger.tsx +64 -64
  300. package/src/select/select.tsx +101 -101
  301. package/src/select/styles.ts +114 -114
  302. package/src/select/types.ts +92 -92
  303. package/src/skeleton/index.ts +2 -2
  304. package/src/skeleton/skeleton.tsx +29 -29
  305. package/src/skeleton/styles.ts +14 -14
  306. package/src/skeleton/types.ts +12 -12
  307. package/src/slider/index.ts +12 -12
  308. package/src/slider/slider-filled-track.tsx +31 -31
  309. package/src/slider/slider-thumb.tsx +52 -52
  310. package/src/slider/slider-track.tsx +154 -154
  311. package/src/slider/slider.tsx +193 -193
  312. package/src/slider/styles.ts +71 -71
  313. package/src/slider/types.ts +47 -47
  314. package/src/snackbar/index.ts +2 -2
  315. package/src/snackbar/snackbar.tsx +39 -39
  316. package/src/snackbar/styles.ts +29 -29
  317. package/src/snackbar/types.ts +21 -21
  318. package/src/spinner/index.ts +2 -2
  319. package/src/spinner/spinner.tsx +29 -29
  320. package/src/spinner/styles.ts +15 -15
  321. package/src/spinner/types.ts +10 -10
  322. package/src/stack/index.ts +2 -2
  323. package/src/stack/stack.tsx +49 -49
  324. package/src/stack/styles.ts +25 -25
  325. package/src/stack/types.ts +15 -15
  326. package/src/stat/index.ts +2 -2
  327. package/src/stat/stat.tsx +48 -48
  328. package/src/stat/styles.ts +34 -34
  329. package/src/stat/types.ts +24 -24
  330. package/src/stepper/index.ts +2 -2
  331. package/src/stepper/stepper.tsx +95 -95
  332. package/src/stepper/styles.ts +49 -49
  333. package/src/stepper/types.ts +20 -20
  334. package/src/switch/index.ts +2 -2
  335. package/src/switch/styles.ts +24 -24
  336. package/src/switch/switch.tsx +67 -67
  337. package/src/switch/types.ts +23 -23
  338. package/src/table/index.ts +2 -2
  339. package/src/table/styles.ts +12 -12
  340. package/src/table/table.tsx +52 -52
  341. package/src/table/types.ts +10 -10
  342. package/src/tabs/index.ts +18 -18
  343. package/src/tabs/styles.ts +113 -113
  344. package/src/tabs/tab-list.tsx +29 -29
  345. package/src/tabs/tab-panel.tsx +29 -29
  346. package/src/tabs/tab-panels.tsx +21 -21
  347. package/src/tabs/tab-text.tsx +26 -26
  348. package/src/tabs/tab.tsx +56 -56
  349. package/src/tabs/tabs.tsx +71 -71
  350. package/src/tabs/types.ts +53 -53
  351. package/src/tag/index.ts +14 -14
  352. package/src/tag/styles.ts +115 -115
  353. package/src/tag/tag-close-button.tsx +26 -26
  354. package/src/tag/tag-icon.tsx +20 -20
  355. package/src/tag/tag-text.tsx +22 -22
  356. package/src/tag/tag.tsx +40 -40
  357. package/src/tag/types.ts +33 -33
  358. package/src/tags-input/index.ts +18 -18
  359. package/src/tags-input/styles.ts +29 -29
  360. package/src/tags-input/tags-input.tsx +149 -149
  361. package/src/tags-input/types.ts +37 -37
  362. package/src/text/index.ts +2 -2
  363. package/src/text/styles.ts +54 -54
  364. package/src/text/text.tsx +51 -51
  365. package/src/text/types.ts +36 -36
  366. package/src/textarea/index.ts +2 -2
  367. package/src/textarea/styles.ts +37 -37
  368. package/src/textarea/textarea.tsx +68 -68
  369. package/src/textarea/types.ts +14 -14
  370. package/src/timeline/index.ts +2 -2
  371. package/src/timeline/styles.ts +57 -57
  372. package/src/timeline/timeline.tsx +52 -52
  373. package/src/timeline/types.ts +30 -30
  374. package/src/toast/index.ts +17 -17
  375. package/src/toast/styles.ts +118 -118
  376. package/src/toast/toast-description.tsx +22 -22
  377. package/src/toast/toast-provider.tsx +136 -136
  378. package/src/toast/toast-title.tsx +22 -22
  379. package/src/toast/toast.tsx +43 -43
  380. package/src/toast/types.ts +50 -50
  381. package/src/toast/use-toast.ts +7 -7
  382. package/src/toggle/index.ts +2 -2
  383. package/src/toggle/styles.ts +30 -30
  384. package/src/toggle/toggle.tsx +25 -25
  385. package/src/toggle/types.ts +15 -15
  386. package/src/toggle-group/index.ts +2 -2
  387. package/src/toggle-group/styles.ts +35 -35
  388. package/src/toggle-group/toggle-group.tsx +60 -60
  389. package/src/toggle-group/types.ts +29 -29
  390. package/src/tooltip/index.ts +11 -11
  391. package/src/tooltip/styles.ts +9 -9
  392. package/src/tooltip/tooltip-content.tsx +19 -19
  393. package/src/tooltip/tooltip-text.tsx +19 -19
  394. package/src/tooltip/tooltip.tsx +116 -116
  395. package/src/tooltip/types.ts +35 -35
  396. package/src/utils/brand.ts +5 -5
  397. package/src/utils/create-context.ts +17 -17
  398. package/src/utils/index.ts +8 -8
  399. package/src/utils/types.ts +20 -20
  400. package/src/visually-hidden/index.ts +2 -2
  401. package/src/visually-hidden/styles.ts +6 -6
  402. package/src/visually-hidden/types.ts +6 -6
  403. package/src/visually-hidden/visually-hidden.tsx +22 -22
  404. package/tailwind-preset.js +212 -203
@@ -1,436 +1,436 @@
1
- import React, { useState, useRef, useCallback, useEffect } from 'react';
2
- import { View, Pressable, Text, Animated, PanResponder, LayoutChangeEvent, Platform } from 'react-native';
3
- import { createComponentContext } from '../utils/create-context';
4
- import type { CarouselProps, CarouselContentProps, CarouselItemProps, CarouselPreviousProps, CarouselNextProps, CarouselDotsProps, CarouselContextValue } from './types';
5
- import { carouselStyle, carouselContentStyle, carouselItemStyle, carouselPreviousStyle, carouselNextStyle, carouselDotsStyle, carouselDotStyle } from './styles';
6
-
7
- export const [CarouselProvider, useCarouselContext] = createComponentContext<CarouselContextValue>('Carousel');
8
-
9
- export const Carousel = React.forwardRef<React.ElementRef<typeof View>, CarouselProps>(
10
- ({ className, defaultIndex = 0, loop = false, onIndexChange, itemWidth: itemWidthProp = 0, gap: gapProp = 0, autoPlay = false, autoPlayInterval = 3000, children, ...props }, ref) => {
11
- const [activeIndex, _setActiveIndex] = useState(defaultIndex);
12
- const [itemCount, setItemCount] = useState(0);
13
- const [width, setWidth] = useState(0);
14
- const activeIndexRef = useRef(defaultIndex);
15
- const itemCountRef = useRef(0);
16
- const widthRef = useRef(0);
17
- const animX = useRef(new Animated.Value(0)).current;
18
- const animXValueRef = useRef(0);
19
-
20
- // Keep prop refs stable for callbacks
21
- const itemWidthRef = useRef(itemWidthProp);
22
- itemWidthRef.current = itemWidthProp;
23
- const gapRef = useRef(gapProp);
24
- gapRef.current = gapProp;
25
-
26
- useEffect(() => {
27
- const id = animX.addListener(({ value }) => { animXValueRef.current = value; });
28
- return () => animX.removeListener(id);
29
- }, [animX]);
30
-
31
- const setActiveIndex = useCallback((index: number) => {
32
- activeIndexRef.current = index;
33
- _setActiveIndex(index);
34
- onIndexChange?.(index);
35
- }, [onIndexChange]);
36
-
37
- /** step = distance between one item start and the next */
38
- const getStep = useCallback(() => {
39
- const iw = itemWidthRef.current;
40
- return iw > 0 ? iw + gapRef.current : widthRef.current;
41
- }, []);
42
-
43
- /** How many items to clone on each side for seamless loop */
44
- const getCloneCount = useCallback(() => {
45
- const iw = itemWidthRef.current;
46
- if (!loop) return 0;
47
- if (iw > 0) {
48
- const step = iw + gapRef.current;
49
- return step > 0 ? Math.ceil(widthRef.current / step) + 1 : 1;
50
- }
51
- return 1;
52
- }, [loop]);
53
-
54
- const goTo = useCallback((index: number, animate = true) => {
55
- const step = getStep();
56
- const clones = getCloneCount();
57
- const targetX = loop ? -(index + clones) * step : -index * step;
58
- setActiveIndex(index);
59
- if (animate) {
60
- Animated.timing(animX, { toValue: targetX, duration: 300, useNativeDriver: true }).start();
61
- } else {
62
- animX.setValue(targetX);
63
- animXValueRef.current = targetX;
64
- }
65
- }, [loop, animX, setActiveIndex, getStep, getCloneCount]);
66
-
67
- const next = useCallback(() => {
68
- const count = itemCountRef.current;
69
- const step = getStep();
70
- const clones = getCloneCount();
71
- if (loop) {
72
- const nextIdx = activeIndexRef.current + 1;
73
- if (nextIdx >= count) {
74
- // Animate into first clone zone, then jump to real first
75
- Animated.timing(animX, { toValue: -(count + clones) * step, duration: 300, useNativeDriver: true }).start(() => {
76
- const jumpX = -clones * step;
77
- animX.setValue(jumpX);
78
- animXValueRef.current = jumpX;
79
- setActiveIndex(0);
80
- });
81
- } else {
82
- goTo(nextIdx);
83
- }
84
- } else {
85
- goTo(Math.min(activeIndexRef.current + 1, count - 1));
86
- }
87
- }, [loop, animX, goTo, setActiveIndex, getStep, getCloneCount]);
88
-
89
- const previous = useCallback(() => {
90
- const count = itemCountRef.current;
91
- const step = getStep();
92
- const clones = getCloneCount();
93
- if (loop) {
94
- const prevIdx = activeIndexRef.current - 1;
95
- if (prevIdx < 0) {
96
- // Animate into last clone zone, then jump to real last
97
- Animated.timing(animX, { toValue: -(clones - 1) * step, duration: 300, useNativeDriver: true }).start(() => {
98
- const jumpX = -(count - 1 + clones) * step;
99
- animX.setValue(jumpX);
100
- animXValueRef.current = jumpX;
101
- setActiveIndex(count - 1);
102
- });
103
- } else {
104
- goTo(prevIdx);
105
- }
106
- } else {
107
- goTo(Math.max(activeIndexRef.current - 1, 0));
108
- }
109
- }, [loop, animX, goTo, setActiveIndex, getStep, getCloneCount]);
110
-
111
- // Auto-play
112
- useEffect(() => {
113
- if (!autoPlay || itemCountRef.current === 0) return;
114
- const id = setInterval(() => { next(); }, autoPlayInterval);
115
- return () => clearInterval(id);
116
- }, [autoPlay, autoPlayInterval, next]);
117
-
118
- return (
119
- <CarouselProvider value={{ activeIndex, setActiveIndex, activeIndexRef, itemCount, setItemCount, itemCountRef, loop, width, setWidth, widthRef, animX, animXValueRef, goTo, next, previous, itemWidth: itemWidthProp, gap: gapProp }}>
120
- <View ref={ref} className={carouselStyle({ class: className })} {...props}>{children}</View>
121
- </CarouselProvider>
122
- );
123
- },
124
- );
125
- Carousel.displayName = 'Carousel';
126
-
127
- export const CarouselContent = React.forwardRef<React.ElementRef<typeof View>, CarouselContentProps>(({ className, children, style, ...props }, ref) => {
128
- const { setWidth, setItemCount, itemCountRef, loop, setActiveIndex, activeIndexRef, animX, animXValueRef, widthRef, itemWidth: itemWidthProp, gap: gapProp } = useCarouselContext();
129
- const dragStartValue = useRef(0);
130
- const dragStartIndex = useRef(0);
131
- const internalRef = useRef<View>(null);
132
-
133
- const loopRef = useRef(loop);
134
- loopRef.current = loop;
135
- const setActiveIndexRef = useRef(setActiveIndex);
136
- setActiveIndexRef.current = setActiveIndex;
137
- const itemWidthRef = useRef(itemWidthProp);
138
- itemWidthRef.current = itemWidthProp;
139
- const gapRef = useRef(gapProp);
140
- gapRef.current = gapProp;
141
-
142
- const childArray = React.Children.toArray(children);
143
- const count = childArray.length;
144
- useEffect(() => {
145
- setItemCount(count);
146
- itemCountRef.current = count;
147
- }, [count]);
148
-
149
- const getStep = () => {
150
- const iw = itemWidthRef.current;
151
- return iw > 0 ? iw + gapRef.current : widthRef.current;
152
- };
153
-
154
- const getCloneCount = () => {
155
- const iw = itemWidthRef.current;
156
- if (!loopRef.current) return 0;
157
- if (iw > 0) {
158
- const step = iw + gapRef.current;
159
- return step > 0 ? Math.ceil(widthRef.current / step) + 1 : 1;
160
- }
161
- return 1;
162
- };
163
-
164
- // Build displayed children with clones for loop mode
165
- const cloneCount = getCloneCount();
166
- let displayedChildren: React.ReactNode;
167
- if (loop && count > 0 && cloneCount > 0) {
168
- const leadClones: React.ReactElement[] = [];
169
- const trailClones: React.ReactElement[] = [];
170
- for (let i = 0; i < cloneCount; i++) {
171
- // Lead clones: last `cloneCount` items
172
- const leadIdx = ((count - cloneCount + i) % count + count) % count;
173
- leadClones.push(
174
- React.cloneElement(childArray[leadIdx] as React.ReactElement, { key: `__clone-lead-${i}__` }),
175
- );
176
- // Trail clones: first `cloneCount` items
177
- const trailIdx = i % count;
178
- trailClones.push(
179
- React.cloneElement(childArray[trailIdx] as React.ReactElement, { key: `__clone-trail-${i}__` }),
180
- );
181
- }
182
- displayedChildren = [...leadClones, ...childArray, ...trailClones];
183
- } else {
184
- displayedChildren = children;
185
- }
186
-
187
- const handleLayout = (e: LayoutChangeEvent) => {
188
- const w = e.nativeEvent.layout.width;
189
- widthRef.current = w;
190
- setWidth(w);
191
- if (loop && w > 0) {
192
- const step = getStep();
193
- const clones = getCloneCount();
194
- const initialX = -clones * step;
195
- animX.setValue(initialX);
196
- animXValueRef.current = initialX;
197
- }
198
- };
199
-
200
- // --- Shared gesture helpers ---
201
- const gestureStart = () => {
202
- animX.stopAnimation();
203
- dragStartValue.current = animXValueRef.current;
204
- dragStartIndex.current = activeIndexRef.current;
205
- };
206
-
207
- const gestureMove = (dx: number) => {
208
- const step = getStep();
209
- const clamped = Math.max(-step, Math.min(step, dx));
210
- animX.setValue(dragStartValue.current + clamped);
211
- };
212
-
213
- const gestureEnd = (dx: number, vx: number) => {
214
- const step = getStep();
215
- if (step === 0) return;
216
- const cnt = itemCountRef.current;
217
- const start = dragStartIndex.current;
218
- const isLoop = loopRef.current;
219
- const setIdx = setActiveIndexRef.current;
220
- const clones = getCloneCount();
221
-
222
- let delta = 0;
223
- if (vx < -0.3 || dx < -step * 0.3) delta = 1;
224
- else if (vx > 0.3 || dx > step * 0.3) delta = -1;
225
-
226
- const targetLogical = start + delta;
227
-
228
- if (isLoop) {
229
- const paddedStart = start + clones;
230
- const targetPage = paddedStart + delta;
231
-
232
- if (targetPage < clones) {
233
- // Went before first real item — animate to clone, then jump
234
- Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start(() => {
235
- const jumpIdx = cnt - 1;
236
- const jumpX = -(jumpIdx + clones) * step;
237
- animX.setValue(jumpX);
238
- animXValueRef.current = jumpX;
239
- setIdx(jumpIdx);
240
- });
241
- } else if (targetPage >= cnt + clones) {
242
- // Went past last real item — animate to clone, then jump
243
- Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start(() => {
244
- const jumpX = -clones * step;
245
- animX.setValue(jumpX);
246
- animXValueRef.current = jumpX;
247
- setIdx(0);
248
- });
249
- } else {
250
- const idx = Math.max(0, Math.min(cnt - 1, targetLogical));
251
- setIdx(idx);
252
- Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start();
253
- }
254
- } else {
255
- const clampedIdx = Math.max(0, Math.min(cnt - 1, targetLogical));
256
- setIdx(clampedIdx);
257
- Animated.timing(animX, { toValue: -clampedIdx * step, duration: 200, useNativeDriver: true }).start();
258
- }
259
- };
260
-
261
- const gestureCancel = () => {
262
- const step = getStep();
263
- if (step === 0) return;
264
- const start = dragStartIndex.current;
265
- const clones = getCloneCount();
266
- const targetPage = loopRef.current ? start + clones : start;
267
- setActiveIndexRef.current(start);
268
- Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start();
269
- };
270
-
271
- // --- Native: PanResponder ---
272
- const panResponder = useRef(
273
- Platform.OS !== 'web'
274
- ? PanResponder.create({
275
- onMoveShouldSetPanResponder: (_, gs) =>
276
- Math.abs(gs.dx) > Math.abs(gs.dy) && Math.abs(gs.dx) > 8,
277
- onPanResponderGrant: gestureStart,
278
- onPanResponderMove: (_, gs) => gestureMove(gs.dx),
279
- onPanResponderRelease: (_, gs) => gestureEnd(gs.dx, gs.vx),
280
- onPanResponderTerminate: gestureCancel,
281
- })
282
- : null
283
- ).current;
284
-
285
- // --- Web: Pointer events ---
286
- useEffect(() => {
287
- if (Platform.OS !== 'web') return;
288
- const el = internalRef.current as unknown as HTMLElement;
289
- if (!el) return;
290
-
291
- let dragging = false;
292
- let startX = 0;
293
- let lastX = 0;
294
- let lastTime = 0;
295
- let moved = false;
296
-
297
- const removeDocListeners = () => {
298
- document.removeEventListener('pointermove', onMove);
299
- document.removeEventListener('pointerup', onUp);
300
- document.removeEventListener('pointercancel', onCancel);
301
- };
302
-
303
- const onDown = (e: PointerEvent) => {
304
- if (e.button !== 0) return;
305
- dragging = true;
306
- moved = false;
307
- startX = e.clientX;
308
- lastX = e.clientX;
309
- lastTime = e.timeStamp;
310
- gestureStart();
311
- document.addEventListener('pointermove', onMove);
312
- document.addEventListener('pointerup', onUp);
313
- document.addEventListener('pointercancel', onCancel);
314
- };
315
-
316
- const onMove = (e: PointerEvent) => {
317
- if (!dragging) return;
318
- const dx = e.clientX - startX;
319
- if (!moved && Math.abs(dx) <= 8) return;
320
- if (!moved) moved = true;
321
- e.preventDefault();
322
- gestureMove(dx);
323
- lastX = e.clientX;
324
- lastTime = e.timeStamp;
325
- };
326
-
327
- const onUp = (e: PointerEvent) => {
328
- if (!dragging) return;
329
- dragging = false;
330
- removeDocListeners();
331
- if (!moved) {
332
- gestureCancel();
333
- return;
334
- }
335
- const dx = e.clientX - startX;
336
- const dt = Math.max(1, e.timeStamp - lastTime);
337
- const vx = (e.clientX - lastX) / dt;
338
- gestureEnd(dx, vx);
339
- };
340
-
341
- const onCancel = () => {
342
- if (!dragging) return;
343
- dragging = false;
344
- removeDocListeners();
345
- gestureCancel();
346
- };
347
-
348
- el.addEventListener('pointerdown', onDown);
349
- return () => {
350
- el.removeEventListener('pointerdown', onDown);
351
- removeDocListeners();
352
- };
353
- }, []);
354
-
355
- const setRef = useCallback((node: View | null) => {
356
- internalRef.current = node;
357
- if (typeof ref === 'function') ref(node);
358
- else if (ref) (ref as React.MutableRefObject<View | null>).current = node;
359
- }, [ref]);
360
-
361
- return (
362
- <View
363
- ref={setRef}
364
- className={carouselContentStyle({ class: className })}
365
- style={[
366
- { overflow: 'hidden' },
367
- Platform.OS === 'web' && ({ touchAction: 'pan-y', userSelect: 'none', cursor: 'grab' } as any),
368
- style,
369
- ]}
370
- onLayout={handleLayout}
371
- {...(panResponder?.panHandlers ?? {})}
372
- {...props}
373
- >
374
- <Animated.View style={{ flexDirection: 'row', gap: gapProp, transform: [{ translateX: animX }] }}>
375
- {displayedChildren}
376
- </Animated.View>
377
- </View>
378
- );
379
- });
380
- CarouselContent.displayName = 'CarouselContent';
381
-
382
- export const CarouselItem = React.forwardRef<React.ElementRef<typeof View>, CarouselItemProps>(({ className, style, onPress, children, ...props }, ref) => {
383
- const { width, itemWidth } = useCarouselContext();
384
- const effectiveWidth = itemWidth > 0 ? itemWidth : width;
385
- return (
386
- <View
387
- ref={ref}
388
- className={carouselItemStyle({ class: className })}
389
- style={[effectiveWidth > 0 ? { width: effectiveWidth } : undefined, style]}
390
- {...props}
391
- >
392
- {onPress ? (
393
- <Pressable onPress={onPress} style={{ flex: 1 }}>
394
- {children}
395
- </Pressable>
396
- ) : (
397
- children
398
- )}
399
- </View>
400
- );
401
- });
402
- CarouselItem.displayName = 'CarouselItem';
403
-
404
- export const CarouselPrevious = React.forwardRef<React.ElementRef<typeof Pressable>, CarouselPreviousProps>(({ className, children, ...props }, ref) => {
405
- const { previous } = useCarouselContext();
406
- return (
407
- <Pressable ref={ref} onPress={previous} className={carouselPreviousStyle({ class: className })} accessibilityRole="button" accessibilityLabel="Previous" {...props}>
408
- {children ?? <Text style={{ fontSize: 16, color: '#374151' }}>{'\u2039'}</Text>}
409
- </Pressable>
410
- );
411
- });
412
- CarouselPrevious.displayName = 'CarouselPrevious';
413
-
414
- export const CarouselNext = React.forwardRef<React.ElementRef<typeof Pressable>, CarouselNextProps>(({ className, children, ...props }, ref) => {
415
- const { next } = useCarouselContext();
416
- return (
417
- <Pressable ref={ref} onPress={next} className={carouselNextStyle({ class: className })} accessibilityRole="button" accessibilityLabel="Next" {...props}>
418
- {children ?? <Text style={{ fontSize: 16, color: '#374151' }}>{'\u203A'}</Text>}
419
- </Pressable>
420
- );
421
- });
422
- CarouselNext.displayName = 'CarouselNext';
423
-
424
- export const CarouselDots = React.forwardRef<React.ElementRef<typeof View>, CarouselDotsProps>(({ className, ...props }, ref) => {
425
- const { activeIndex, itemCount, goTo } = useCarouselContext();
426
- return (
427
- <View ref={ref} className={carouselDotsStyle({ class: className })} {...props}>
428
- {Array.from({ length: itemCount }, (_, i) => (
429
- <Pressable key={i} onPress={() => goTo(i)} accessibilityRole="button" accessibilityLabel={`Go to slide ${i + 1}`}>
430
- <View className={carouselDotStyle({ isActive: i === activeIndex })} />
431
- </Pressable>
432
- ))}
433
- </View>
434
- );
435
- });
436
- CarouselDots.displayName = 'CarouselDots';
1
+ import React, { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { View, Pressable, Text, Animated, PanResponder, LayoutChangeEvent, Platform } from 'react-native';
3
+ import { createComponentContext } from '../utils/create-context';
4
+ import type { CarouselProps, CarouselContentProps, CarouselItemProps, CarouselPreviousProps, CarouselNextProps, CarouselDotsProps, CarouselContextValue } from './types';
5
+ import { carouselStyle, carouselContentStyle, carouselItemStyle, carouselPreviousStyle, carouselNextStyle, carouselDotsStyle, carouselDotStyle } from './styles';
6
+
7
+ export const [CarouselProvider, useCarouselContext] = createComponentContext<CarouselContextValue>('Carousel');
8
+
9
+ export const Carousel = React.forwardRef<React.ElementRef<typeof View>, CarouselProps>(
10
+ ({ className, defaultIndex = 0, loop = false, onIndexChange, itemWidth: itemWidthProp = 0, gap: gapProp = 0, autoPlay = false, autoPlayInterval = 3000, children, ...props }, ref) => {
11
+ const [activeIndex, _setActiveIndex] = useState(defaultIndex);
12
+ const [itemCount, setItemCount] = useState(0);
13
+ const [width, setWidth] = useState(0);
14
+ const activeIndexRef = useRef(defaultIndex);
15
+ const itemCountRef = useRef(0);
16
+ const widthRef = useRef(0);
17
+ const animX = useRef(new Animated.Value(0)).current;
18
+ const animXValueRef = useRef(0);
19
+
20
+ // Keep prop refs stable for callbacks
21
+ const itemWidthRef = useRef(itemWidthProp);
22
+ itemWidthRef.current = itemWidthProp;
23
+ const gapRef = useRef(gapProp);
24
+ gapRef.current = gapProp;
25
+
26
+ useEffect(() => {
27
+ const id = animX.addListener(({ value }) => { animXValueRef.current = value; });
28
+ return () => animX.removeListener(id);
29
+ }, [animX]);
30
+
31
+ const setActiveIndex = useCallback((index: number) => {
32
+ activeIndexRef.current = index;
33
+ _setActiveIndex(index);
34
+ onIndexChange?.(index);
35
+ }, [onIndexChange]);
36
+
37
+ /** step = distance between one item start and the next */
38
+ const getStep = useCallback(() => {
39
+ const iw = itemWidthRef.current;
40
+ return iw > 0 ? iw + gapRef.current : widthRef.current;
41
+ }, []);
42
+
43
+ /** How many items to clone on each side for seamless loop */
44
+ const getCloneCount = useCallback(() => {
45
+ const iw = itemWidthRef.current;
46
+ if (!loop) return 0;
47
+ if (iw > 0) {
48
+ const step = iw + gapRef.current;
49
+ return step > 0 ? Math.ceil(widthRef.current / step) + 1 : 1;
50
+ }
51
+ return 1;
52
+ }, [loop]);
53
+
54
+ const goTo = useCallback((index: number, animate = true) => {
55
+ const step = getStep();
56
+ const clones = getCloneCount();
57
+ const targetX = loop ? -(index + clones) * step : -index * step;
58
+ setActiveIndex(index);
59
+ if (animate) {
60
+ Animated.timing(animX, { toValue: targetX, duration: 300, useNativeDriver: true }).start();
61
+ } else {
62
+ animX.setValue(targetX);
63
+ animXValueRef.current = targetX;
64
+ }
65
+ }, [loop, animX, setActiveIndex, getStep, getCloneCount]);
66
+
67
+ const next = useCallback(() => {
68
+ const count = itemCountRef.current;
69
+ const step = getStep();
70
+ const clones = getCloneCount();
71
+ if (loop) {
72
+ const nextIdx = activeIndexRef.current + 1;
73
+ if (nextIdx >= count) {
74
+ // Animate into first clone zone, then jump to real first
75
+ Animated.timing(animX, { toValue: -(count + clones) * step, duration: 300, useNativeDriver: true }).start(() => {
76
+ const jumpX = -clones * step;
77
+ animX.setValue(jumpX);
78
+ animXValueRef.current = jumpX;
79
+ setActiveIndex(0);
80
+ });
81
+ } else {
82
+ goTo(nextIdx);
83
+ }
84
+ } else {
85
+ goTo(Math.min(activeIndexRef.current + 1, count - 1));
86
+ }
87
+ }, [loop, animX, goTo, setActiveIndex, getStep, getCloneCount]);
88
+
89
+ const previous = useCallback(() => {
90
+ const count = itemCountRef.current;
91
+ const step = getStep();
92
+ const clones = getCloneCount();
93
+ if (loop) {
94
+ const prevIdx = activeIndexRef.current - 1;
95
+ if (prevIdx < 0) {
96
+ // Animate into last clone zone, then jump to real last
97
+ Animated.timing(animX, { toValue: -(clones - 1) * step, duration: 300, useNativeDriver: true }).start(() => {
98
+ const jumpX = -(count - 1 + clones) * step;
99
+ animX.setValue(jumpX);
100
+ animXValueRef.current = jumpX;
101
+ setActiveIndex(count - 1);
102
+ });
103
+ } else {
104
+ goTo(prevIdx);
105
+ }
106
+ } else {
107
+ goTo(Math.max(activeIndexRef.current - 1, 0));
108
+ }
109
+ }, [loop, animX, goTo, setActiveIndex, getStep, getCloneCount]);
110
+
111
+ // Auto-play
112
+ useEffect(() => {
113
+ if (!autoPlay || itemCountRef.current === 0) return;
114
+ const id = setInterval(() => { next(); }, autoPlayInterval);
115
+ return () => clearInterval(id);
116
+ }, [autoPlay, autoPlayInterval, next]);
117
+
118
+ return (
119
+ <CarouselProvider value={{ activeIndex, setActiveIndex, activeIndexRef, itemCount, setItemCount, itemCountRef, loop, width, setWidth, widthRef, animX, animXValueRef, goTo, next, previous, itemWidth: itemWidthProp, gap: gapProp }}>
120
+ <View ref={ref} className={carouselStyle({ class: className })} {...props}>{children}</View>
121
+ </CarouselProvider>
122
+ );
123
+ },
124
+ );
125
+ Carousel.displayName = 'Carousel';
126
+
127
+ export const CarouselContent = React.forwardRef<React.ElementRef<typeof View>, CarouselContentProps>(({ className, children, style, ...props }, ref) => {
128
+ const { setWidth, setItemCount, itemCountRef, loop, setActiveIndex, activeIndexRef, animX, animXValueRef, widthRef, itemWidth: itemWidthProp, gap: gapProp } = useCarouselContext();
129
+ const dragStartValue = useRef(0);
130
+ const dragStartIndex = useRef(0);
131
+ const internalRef = useRef<View>(null);
132
+
133
+ const loopRef = useRef(loop);
134
+ loopRef.current = loop;
135
+ const setActiveIndexRef = useRef(setActiveIndex);
136
+ setActiveIndexRef.current = setActiveIndex;
137
+ const itemWidthRef = useRef(itemWidthProp);
138
+ itemWidthRef.current = itemWidthProp;
139
+ const gapRef = useRef(gapProp);
140
+ gapRef.current = gapProp;
141
+
142
+ const childArray = React.Children.toArray(children);
143
+ const count = childArray.length;
144
+ useEffect(() => {
145
+ setItemCount(count);
146
+ itemCountRef.current = count;
147
+ }, [count]);
148
+
149
+ const getStep = () => {
150
+ const iw = itemWidthRef.current;
151
+ return iw > 0 ? iw + gapRef.current : widthRef.current;
152
+ };
153
+
154
+ const getCloneCount = () => {
155
+ const iw = itemWidthRef.current;
156
+ if (!loopRef.current) return 0;
157
+ if (iw > 0) {
158
+ const step = iw + gapRef.current;
159
+ return step > 0 ? Math.ceil(widthRef.current / step) + 1 : 1;
160
+ }
161
+ return 1;
162
+ };
163
+
164
+ // Build displayed children with clones for loop mode
165
+ const cloneCount = getCloneCount();
166
+ let displayedChildren: React.ReactNode;
167
+ if (loop && count > 0 && cloneCount > 0) {
168
+ const leadClones: React.ReactElement[] = [];
169
+ const trailClones: React.ReactElement[] = [];
170
+ for (let i = 0; i < cloneCount; i++) {
171
+ // Lead clones: last `cloneCount` items
172
+ const leadIdx = ((count - cloneCount + i) % count + count) % count;
173
+ leadClones.push(
174
+ React.cloneElement(childArray[leadIdx] as React.ReactElement, { key: `__clone-lead-${i}__` }),
175
+ );
176
+ // Trail clones: first `cloneCount` items
177
+ const trailIdx = i % count;
178
+ trailClones.push(
179
+ React.cloneElement(childArray[trailIdx] as React.ReactElement, { key: `__clone-trail-${i}__` }),
180
+ );
181
+ }
182
+ displayedChildren = [...leadClones, ...childArray, ...trailClones];
183
+ } else {
184
+ displayedChildren = children;
185
+ }
186
+
187
+ const handleLayout = (e: LayoutChangeEvent) => {
188
+ const w = e.nativeEvent.layout.width;
189
+ widthRef.current = w;
190
+ setWidth(w);
191
+ if (loop && w > 0) {
192
+ const step = getStep();
193
+ const clones = getCloneCount();
194
+ const initialX = -clones * step;
195
+ animX.setValue(initialX);
196
+ animXValueRef.current = initialX;
197
+ }
198
+ };
199
+
200
+ // --- Shared gesture helpers ---
201
+ const gestureStart = () => {
202
+ animX.stopAnimation();
203
+ dragStartValue.current = animXValueRef.current;
204
+ dragStartIndex.current = activeIndexRef.current;
205
+ };
206
+
207
+ const gestureMove = (dx: number) => {
208
+ const step = getStep();
209
+ const clamped = Math.max(-step, Math.min(step, dx));
210
+ animX.setValue(dragStartValue.current + clamped);
211
+ };
212
+
213
+ const gestureEnd = (dx: number, vx: number) => {
214
+ const step = getStep();
215
+ if (step === 0) return;
216
+ const cnt = itemCountRef.current;
217
+ const start = dragStartIndex.current;
218
+ const isLoop = loopRef.current;
219
+ const setIdx = setActiveIndexRef.current;
220
+ const clones = getCloneCount();
221
+
222
+ let delta = 0;
223
+ if (vx < -0.3 || dx < -step * 0.3) delta = 1;
224
+ else if (vx > 0.3 || dx > step * 0.3) delta = -1;
225
+
226
+ const targetLogical = start + delta;
227
+
228
+ if (isLoop) {
229
+ const paddedStart = start + clones;
230
+ const targetPage = paddedStart + delta;
231
+
232
+ if (targetPage < clones) {
233
+ // Went before first real item — animate to clone, then jump
234
+ Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start(() => {
235
+ const jumpIdx = cnt - 1;
236
+ const jumpX = -(jumpIdx + clones) * step;
237
+ animX.setValue(jumpX);
238
+ animXValueRef.current = jumpX;
239
+ setIdx(jumpIdx);
240
+ });
241
+ } else if (targetPage >= cnt + clones) {
242
+ // Went past last real item — animate to clone, then jump
243
+ Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start(() => {
244
+ const jumpX = -clones * step;
245
+ animX.setValue(jumpX);
246
+ animXValueRef.current = jumpX;
247
+ setIdx(0);
248
+ });
249
+ } else {
250
+ const idx = Math.max(0, Math.min(cnt - 1, targetLogical));
251
+ setIdx(idx);
252
+ Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start();
253
+ }
254
+ } else {
255
+ const clampedIdx = Math.max(0, Math.min(cnt - 1, targetLogical));
256
+ setIdx(clampedIdx);
257
+ Animated.timing(animX, { toValue: -clampedIdx * step, duration: 200, useNativeDriver: true }).start();
258
+ }
259
+ };
260
+
261
+ const gestureCancel = () => {
262
+ const step = getStep();
263
+ if (step === 0) return;
264
+ const start = dragStartIndex.current;
265
+ const clones = getCloneCount();
266
+ const targetPage = loopRef.current ? start + clones : start;
267
+ setActiveIndexRef.current(start);
268
+ Animated.timing(animX, { toValue: -targetPage * step, duration: 200, useNativeDriver: true }).start();
269
+ };
270
+
271
+ // --- Native: PanResponder ---
272
+ const panResponder = useRef(
273
+ Platform.OS !== 'web'
274
+ ? PanResponder.create({
275
+ onMoveShouldSetPanResponder: (_, gs) =>
276
+ Math.abs(gs.dx) > Math.abs(gs.dy) && Math.abs(gs.dx) > 8,
277
+ onPanResponderGrant: gestureStart,
278
+ onPanResponderMove: (_, gs) => gestureMove(gs.dx),
279
+ onPanResponderRelease: (_, gs) => gestureEnd(gs.dx, gs.vx),
280
+ onPanResponderTerminate: gestureCancel,
281
+ })
282
+ : null
283
+ ).current;
284
+
285
+ // --- Web: Pointer events ---
286
+ useEffect(() => {
287
+ if (Platform.OS !== 'web') return;
288
+ const el = internalRef.current as unknown as HTMLElement;
289
+ if (!el) return;
290
+
291
+ let dragging = false;
292
+ let startX = 0;
293
+ let lastX = 0;
294
+ let lastTime = 0;
295
+ let moved = false;
296
+
297
+ const removeDocListeners = () => {
298
+ document.removeEventListener('pointermove', onMove);
299
+ document.removeEventListener('pointerup', onUp);
300
+ document.removeEventListener('pointercancel', onCancel);
301
+ };
302
+
303
+ const onDown = (e: PointerEvent) => {
304
+ if (e.button !== 0) return;
305
+ dragging = true;
306
+ moved = false;
307
+ startX = e.clientX;
308
+ lastX = e.clientX;
309
+ lastTime = e.timeStamp;
310
+ gestureStart();
311
+ document.addEventListener('pointermove', onMove);
312
+ document.addEventListener('pointerup', onUp);
313
+ document.addEventListener('pointercancel', onCancel);
314
+ };
315
+
316
+ const onMove = (e: PointerEvent) => {
317
+ if (!dragging) return;
318
+ const dx = e.clientX - startX;
319
+ if (!moved && Math.abs(dx) <= 8) return;
320
+ if (!moved) moved = true;
321
+ e.preventDefault();
322
+ gestureMove(dx);
323
+ lastX = e.clientX;
324
+ lastTime = e.timeStamp;
325
+ };
326
+
327
+ const onUp = (e: PointerEvent) => {
328
+ if (!dragging) return;
329
+ dragging = false;
330
+ removeDocListeners();
331
+ if (!moved) {
332
+ gestureCancel();
333
+ return;
334
+ }
335
+ const dx = e.clientX - startX;
336
+ const dt = Math.max(1, e.timeStamp - lastTime);
337
+ const vx = (e.clientX - lastX) / dt;
338
+ gestureEnd(dx, vx);
339
+ };
340
+
341
+ const onCancel = () => {
342
+ if (!dragging) return;
343
+ dragging = false;
344
+ removeDocListeners();
345
+ gestureCancel();
346
+ };
347
+
348
+ el.addEventListener('pointerdown', onDown);
349
+ return () => {
350
+ el.removeEventListener('pointerdown', onDown);
351
+ removeDocListeners();
352
+ };
353
+ }, []);
354
+
355
+ const setRef = useCallback((node: View | null) => {
356
+ internalRef.current = node;
357
+ if (typeof ref === 'function') ref(node);
358
+ else if (ref) (ref as React.MutableRefObject<View | null>).current = node;
359
+ }, [ref]);
360
+
361
+ return (
362
+ <View
363
+ ref={setRef}
364
+ className={carouselContentStyle({ class: className })}
365
+ style={[
366
+ { overflow: 'hidden' },
367
+ Platform.OS === 'web' && ({ touchAction: 'pan-y', userSelect: 'none', cursor: 'grab' } as any),
368
+ style,
369
+ ]}
370
+ onLayout={handleLayout}
371
+ {...(panResponder?.panHandlers ?? {})}
372
+ {...props}
373
+ >
374
+ <Animated.View style={{ flexDirection: 'row', gap: gapProp, transform: [{ translateX: animX }] }}>
375
+ {displayedChildren}
376
+ </Animated.View>
377
+ </View>
378
+ );
379
+ });
380
+ CarouselContent.displayName = 'CarouselContent';
381
+
382
+ export const CarouselItem = React.forwardRef<React.ElementRef<typeof View>, CarouselItemProps>(({ className, style, onPress, children, ...props }, ref) => {
383
+ const { width, itemWidth } = useCarouselContext();
384
+ const effectiveWidth = itemWidth > 0 ? itemWidth : width;
385
+ return (
386
+ <View
387
+ ref={ref}
388
+ className={carouselItemStyle({ class: className })}
389
+ style={[effectiveWidth > 0 ? { width: effectiveWidth } : undefined, style]}
390
+ {...props}
391
+ >
392
+ {onPress ? (
393
+ <Pressable onPress={onPress} style={{ flex: 1 }}>
394
+ {children}
395
+ </Pressable>
396
+ ) : (
397
+ children
398
+ )}
399
+ </View>
400
+ );
401
+ });
402
+ CarouselItem.displayName = 'CarouselItem';
403
+
404
+ export const CarouselPrevious = React.forwardRef<React.ElementRef<typeof Pressable>, CarouselPreviousProps>(({ className, children, ...props }, ref) => {
405
+ const { previous } = useCarouselContext();
406
+ return (
407
+ <Pressable ref={ref} onPress={previous} className={carouselPreviousStyle({ class: className })} accessibilityRole="button" accessibilityLabel="Previous" {...props}>
408
+ {children ?? <Text style={{ fontSize: 16, color: '#374151' }}>{'\u2039'}</Text>}
409
+ </Pressable>
410
+ );
411
+ });
412
+ CarouselPrevious.displayName = 'CarouselPrevious';
413
+
414
+ export const CarouselNext = React.forwardRef<React.ElementRef<typeof Pressable>, CarouselNextProps>(({ className, children, ...props }, ref) => {
415
+ const { next } = useCarouselContext();
416
+ return (
417
+ <Pressable ref={ref} onPress={next} className={carouselNextStyle({ class: className })} accessibilityRole="button" accessibilityLabel="Next" {...props}>
418
+ {children ?? <Text style={{ fontSize: 16, color: '#374151' }}>{'\u203A'}</Text>}
419
+ </Pressable>
420
+ );
421
+ });
422
+ CarouselNext.displayName = 'CarouselNext';
423
+
424
+ export const CarouselDots = React.forwardRef<React.ElementRef<typeof View>, CarouselDotsProps>(({ className, ...props }, ref) => {
425
+ const { activeIndex, itemCount, goTo } = useCarouselContext();
426
+ return (
427
+ <View ref={ref} className={carouselDotsStyle({ class: className })} {...props}>
428
+ {Array.from({ length: itemCount }, (_, i) => (
429
+ <Pressable key={i} onPress={() => goTo(i)} accessibilityRole="button" accessibilityLabel={`Go to slide ${i + 1}`}>
430
+ <View className={carouselDotStyle({ isActive: i === activeIndex })} />
431
+ </Pressable>
432
+ ))}
433
+ </View>
434
+ );
435
+ });
436
+ CarouselDots.displayName = 'CarouselDots';