@wireservers-ui/react-natives 1.0.0 → 1.0.1-rc2

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 (402) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -98
  3. package/bin/cli.js +374 -0
  4. package/package.json +27 -48
  5. package/src/accordion/accordion-content.tsx +30 -30
  6. package/src/accordion/accordion-icon.tsx +54 -54
  7. package/src/accordion/accordion-item.tsx +37 -37
  8. package/src/accordion/accordion-title-text.tsx +24 -24
  9. package/src/accordion/accordion-trigger.tsx +38 -38
  10. package/src/accordion/accordion.tsx +91 -91
  11. package/src/accordion/index.ts +24 -24
  12. package/src/accordion/styles.ts +74 -74
  13. package/src/accordion/types.ts +56 -56
  14. package/src/actionsheet/actionsheet-backdrop.tsx +23 -23
  15. package/src/actionsheet/actionsheet-content.tsx +19 -19
  16. package/src/actionsheet/actionsheet-drag-indicator-wrapper.tsx +19 -19
  17. package/src/actionsheet/actionsheet-drag-indicator.tsx +19 -19
  18. package/src/actionsheet/actionsheet-item-text.tsx +19 -19
  19. package/src/actionsheet/actionsheet-item.tsx +20 -20
  20. package/src/actionsheet/actionsheet-scroll-view.tsx +12 -12
  21. package/src/actionsheet/actionsheet.tsx +45 -45
  22. package/src/actionsheet/index.ts +20 -20
  23. package/src/actionsheet/styles.ts +25 -25
  24. package/src/actionsheet/types.ts +49 -49
  25. package/src/alert/alert-body.tsx +19 -19
  26. package/src/alert/alert-close-button.tsx +23 -23
  27. package/src/alert/alert-icon.tsx +40 -40
  28. package/src/alert/alert-text.tsx +22 -22
  29. package/src/alert/alert.tsx +33 -33
  30. package/src/alert/index.ts +15 -15
  31. package/src/alert/styles.ts +112 -112
  32. package/src/alert/types.ts +36 -36
  33. package/src/alert-dialog/alert-dialog.tsx +54 -54
  34. package/src/alert-dialog/index.ts +2 -2
  35. package/src/alert-dialog/styles.ts +40 -40
  36. package/src/alert-dialog/types.ts +40 -40
  37. package/src/aspect-ratio/aspect-ratio.tsx +20 -20
  38. package/src/aspect-ratio/index.ts +2 -2
  39. package/src/aspect-ratio/styles.ts +6 -6
  40. package/src/aspect-ratio/types.ts +7 -7
  41. package/src/avatar/avatar-badge.tsx +22 -22
  42. package/src/avatar/avatar-fallback-text.tsx +33 -33
  43. package/src/avatar/avatar-group.tsx +53 -53
  44. package/src/avatar/avatar-image.tsx +21 -21
  45. package/src/avatar/avatar.tsx +27 -27
  46. package/src/avatar/index.ts +14 -14
  47. package/src/avatar/styles.ts +94 -94
  48. package/src/avatar/types.ts +35 -35
  49. package/src/badge/badge-icon.tsx +20 -20
  50. package/src/badge/badge-text.tsx +24 -24
  51. package/src/badge/badge.tsx +39 -39
  52. package/src/badge/index.ts +11 -11
  53. package/src/badge/styles.ts +175 -175
  54. package/src/badge/types.ts +37 -37
  55. package/src/blockquote/blockquote.tsx +21 -21
  56. package/src/blockquote/index.ts +2 -2
  57. package/src/blockquote/styles.ts +11 -11
  58. package/src/blockquote/types.ts +6 -6
  59. package/src/box/box.tsx +19 -19
  60. package/src/box/index.ts +2 -2
  61. package/src/box/styles.ts +6 -6
  62. package/src/box/types.ts +6 -6
  63. package/src/breadcrumb/breadcrumb-item.tsx +20 -20
  64. package/src/breadcrumb/breadcrumb-link.tsx +20 -20
  65. package/src/breadcrumb/breadcrumb-text.tsx +19 -19
  66. package/src/breadcrumb/breadcrumb.tsx +43 -43
  67. package/src/breadcrumb/index.ts +12 -12
  68. package/src/breadcrumb/styles.ts +36 -36
  69. package/src/breadcrumb/types.ts +33 -33
  70. package/src/button/button-group.tsx +35 -35
  71. package/src/button/button-icon.tsx +37 -37
  72. package/src/button/button-spinner.tsx +12 -12
  73. package/src/button/button-text.tsx +27 -27
  74. package/src/button/button.tsx +42 -42
  75. package/src/button/index.ts +19 -19
  76. package/src/button/styles.ts +250 -250
  77. package/src/button/types.ts +67 -67
  78. package/src/calendar/calendar-day-cell.tsx +67 -67
  79. package/src/calendar/calendar-day-view.tsx +66 -66
  80. package/src/calendar/calendar-event.tsx +59 -59
  81. package/src/calendar/calendar-header.tsx +60 -60
  82. package/src/calendar/calendar-horizontal-view.tsx +372 -372
  83. package/src/calendar/calendar-legend.tsx +41 -41
  84. package/src/calendar/calendar-month-view.tsx +47 -47
  85. package/src/calendar/calendar-vertical-view.tsx +395 -395
  86. package/src/calendar/calendar-view-switcher.tsx +65 -65
  87. package/src/calendar/calendar-week-view.tsx +52 -52
  88. package/src/calendar/calendar.tsx +74 -74
  89. package/src/calendar/index.ts +27 -27
  90. package/src/calendar/styles.ts +367 -367
  91. package/src/calendar/types.ts +101 -101
  92. package/src/calendar/use-calendar.ts +170 -170
  93. package/src/calendar/utils.ts +278 -278
  94. package/src/card/card-body.tsx +22 -22
  95. package/src/card/card-footer.tsx +19 -19
  96. package/src/card/card-header.tsx +22 -22
  97. package/src/card/card.tsx +27 -27
  98. package/src/card/index.ts +13 -13
  99. package/src/card/styles.ts +54 -54
  100. package/src/card/types.ts +31 -31
  101. package/src/carousel/carousel.tsx +436 -436
  102. package/src/carousel/index.ts +2 -2
  103. package/src/carousel/styles.ts +21 -21
  104. package/src/carousel/types.ts +41 -41
  105. package/src/center/center.tsx +19 -19
  106. package/src/center/index.ts +2 -2
  107. package/src/center/styles.ts +6 -6
  108. package/src/center/types.ts +6 -6
  109. package/src/checkbox/checkbox-group.tsx +63 -63
  110. package/src/checkbox/checkbox-icon.tsx +35 -35
  111. package/src/checkbox/checkbox-indicator.tsx +30 -30
  112. package/src/checkbox/checkbox-label.tsx +24 -24
  113. package/src/checkbox/checkbox.tsx +86 -86
  114. package/src/checkbox/index.ts +14 -14
  115. package/src/checkbox/styles.ts +69 -69
  116. package/src/checkbox/types.ts +55 -55
  117. package/src/circular-progress/circular-progress.tsx +82 -82
  118. package/src/circular-progress/index.ts +2 -2
  119. package/src/circular-progress/styles.ts +31 -31
  120. package/src/circular-progress/types.ts +18 -18
  121. package/src/code/code.tsx +36 -36
  122. package/src/code/index.ts +2 -2
  123. package/src/code/styles.ts +25 -25
  124. package/src/code/types.ts +13 -13
  125. package/src/collapsible/collapsible.tsx +58 -58
  126. package/src/collapsible/index.ts +2 -2
  127. package/src/collapsible/styles.ts +5 -5
  128. package/src/collapsible/types.ts +21 -21
  129. package/src/color-picker/color-picker-box.tsx +115 -115
  130. package/src/color-picker/color-picker-slider.tsx +98 -98
  131. package/src/color-picker/color-picker.tsx +162 -162
  132. package/src/color-picker/color-utils.ts +215 -215
  133. package/src/color-picker/index.ts +34 -34
  134. package/src/color-picker/styles.ts +32 -32
  135. package/src/color-picker/types.ts +49 -49
  136. package/src/color-picker/use-pointer-drag.ts +80 -80
  137. package/src/container/container.tsx +19 -19
  138. package/src/container/index.ts +2 -2
  139. package/src/container/styles.ts +21 -21
  140. package/src/container/types.ts +10 -10
  141. package/src/date-picker/date-picker.tsx +136 -136
  142. package/src/date-picker/index.ts +15 -15
  143. package/src/date-picker/styles.ts +18 -18
  144. package/src/date-picker/types.ts +33 -33
  145. package/src/divider/divider.tsx +21 -21
  146. package/src/divider/index.ts +2 -2
  147. package/src/divider/styles.ts +14 -14
  148. package/src/divider/types.ts +7 -7
  149. package/src/drawer/drawer-backdrop.tsx +23 -23
  150. package/src/drawer/drawer-body.tsx +19 -19
  151. package/src/drawer/drawer-close-button.tsx +29 -29
  152. package/src/drawer/drawer-content.tsx +142 -142
  153. package/src/drawer/drawer-footer.tsx +19 -19
  154. package/src/drawer/drawer-header.tsx +19 -19
  155. package/src/drawer/drawer.tsx +54 -54
  156. package/src/drawer/index.ts +22 -22
  157. package/src/drawer/styles.ts +36 -36
  158. package/src/drawer/types.ts +62 -62
  159. package/src/empty/empty.tsx +53 -53
  160. package/src/empty/index.ts +2 -2
  161. package/src/empty/styles.ts +26 -26
  162. package/src/empty/types.ts +22 -22
  163. package/src/fab/fab-icon.tsx +20 -20
  164. package/src/fab/fab-label.tsx +22 -22
  165. package/src/fab/fab.tsx +45 -45
  166. package/src/fab/index.ts +11 -11
  167. package/src/fab/styles.ts +57 -57
  168. package/src/fab/types.ts +33 -33
  169. package/src/form-control/form-control-error-icon.tsx +25 -25
  170. package/src/form-control/form-control-error-message.tsx +40 -40
  171. package/src/form-control/form-control-helper-text.tsx +25 -25
  172. package/src/form-control/form-control-label-text.tsx +25 -25
  173. package/src/form-control/form-control-label.tsx +36 -36
  174. package/src/form-control/form-control.tsx +46 -46
  175. package/src/form-control/index.ts +20 -20
  176. package/src/form-control/styles.ts +105 -105
  177. package/src/form-control/types.ts +45 -45
  178. package/src/heading/heading.tsx +21 -21
  179. package/src/heading/index.ts +2 -2
  180. package/src/heading/styles.ts +24 -24
  181. package/src/heading/types.ts +19 -19
  182. package/src/icon/icon.tsx +21 -21
  183. package/src/icon/index.ts +2 -2
  184. package/src/icon/styles.ts +18 -18
  185. package/src/icon/types.ts +8 -8
  186. package/src/icon-button/icon-button.tsx +23 -23
  187. package/src/icon-button/index.ts +2 -2
  188. package/src/icon-button/styles.ts +78 -78
  189. package/src/icon-button/types.ts +15 -15
  190. package/src/image/image.tsx +20 -20
  191. package/src/image/index.ts +2 -2
  192. package/src/image/styles.ts +28 -28
  193. package/src/image/types.ts +11 -11
  194. package/src/index.ts +1039 -1039
  195. package/src/input/index.ts +13 -13
  196. package/src/input/input-field.tsx +35 -35
  197. package/src/input/input-icon.tsx +25 -25
  198. package/src/input/input-slot.tsx +24 -24
  199. package/src/input/input.tsx +73 -73
  200. package/src/input/styles.ts +90 -90
  201. package/src/input/types.ts +39 -39
  202. package/src/kbd/index.ts +2 -2
  203. package/src/kbd/kbd.tsx +21 -21
  204. package/src/kbd/styles.ts +11 -11
  205. package/src/kbd/types.ts +7 -7
  206. package/src/link/index.ts +4 -4
  207. package/src/link/link-text.tsx +19 -19
  208. package/src/link/link.tsx +31 -31
  209. package/src/link/styles.ts +19 -19
  210. package/src/link/types.ts +13 -13
  211. package/src/list/index.ts +2 -2
  212. package/src/list/list.tsx +55 -55
  213. package/src/list/styles.ts +8 -8
  214. package/src/list/types.ts +17 -17
  215. package/src/menu/index.ts +2 -2
  216. package/src/menu/menu.tsx +99 -99
  217. package/src/menu/styles.ts +14 -14
  218. package/src/menu/types.ts +30 -30
  219. package/src/modal/index.ts +18 -18
  220. package/src/modal/modal-backdrop.tsx +23 -23
  221. package/src/modal/modal-body.tsx +19 -19
  222. package/src/modal/modal-close-button.tsx +29 -29
  223. package/src/modal/modal-content.tsx +22 -22
  224. package/src/modal/modal-footer.tsx +19 -19
  225. package/src/modal/modal-header.tsx +19 -19
  226. package/src/modal/modal.tsx +50 -50
  227. package/src/modal/styles.ts +37 -37
  228. package/src/modal/types.ts +49 -49
  229. package/src/nativewind-env.d.ts +1 -1
  230. package/src/number-input/index.ts +18 -18
  231. package/src/number-input/number-input.tsx +161 -161
  232. package/src/number-input/styles.ts +35 -35
  233. package/src/number-input/types.ts +44 -44
  234. package/src/overlay/index.ts +2 -2
  235. package/src/overlay/overlay.tsx +21 -21
  236. package/src/overlay/styles.ts +6 -6
  237. package/src/overlay/types.ts +7 -7
  238. package/src/pagination/index.ts +2 -2
  239. package/src/pagination/pagination.tsx +58 -58
  240. package/src/pagination/styles.ts +27 -27
  241. package/src/pagination/types.ts +19 -19
  242. package/src/password-input/index.ts +14 -14
  243. package/src/password-input/password-input.tsx +79 -79
  244. package/src/password-input/styles.ts +25 -25
  245. package/src/password-input/types.ts +24 -24
  246. package/src/pin-input/index.ts +12 -12
  247. package/src/pin-input/pin-input.tsx +96 -96
  248. package/src/pin-input/styles.ts +16 -16
  249. package/src/pin-input/types.ts +26 -26
  250. package/src/popover/index.ts +2 -2
  251. package/src/popover/popover.tsx +98 -98
  252. package/src/popover/styles.ts +31 -31
  253. package/src/popover/types.ts +46 -46
  254. package/src/portal/index.ts +2 -2
  255. package/src/portal/portal.tsx +8 -8
  256. package/src/portal/styles.ts +2 -2
  257. package/src/portal/types.ts +3 -3
  258. package/src/pressable/index.ts +2 -2
  259. package/src/pressable/pressable.tsx +20 -20
  260. package/src/pressable/styles.ts +10 -10
  261. package/src/pressable/types.ts +6 -6
  262. package/src/progress/index.ts +9 -9
  263. package/src/progress/progress-filled-track.tsx +26 -26
  264. package/src/progress/progress.tsx +52 -52
  265. package/src/progress/styles.ts +34 -34
  266. package/src/progress/types.ts +28 -28
  267. package/src/radio/index.ts +14 -14
  268. package/src/radio/radio-group.tsx +61 -61
  269. package/src/radio/radio-icon.tsx +24 -24
  270. package/src/radio/radio-indicator.tsx +30 -30
  271. package/src/radio/radio-label.tsx +24 -24
  272. package/src/radio/radio.tsx +68 -68
  273. package/src/radio/styles.ts +69 -69
  274. package/src/radio/types.ts +51 -51
  275. package/src/rating/index.ts +7 -7
  276. package/src/rating/rating.tsx +93 -93
  277. package/src/rating/styles.ts +13 -13
  278. package/src/rating/types.ts +29 -29
  279. package/src/search-input/index.ts +16 -16
  280. package/src/search-input/search-input.tsx +119 -119
  281. package/src/search-input/styles.ts +28 -28
  282. package/src/search-input/types.ts +31 -31
  283. package/src/segmented-control/index.ts +2 -2
  284. package/src/segmented-control/segmented-control.tsx +34 -34
  285. package/src/segmented-control/styles.ts +22 -22
  286. package/src/segmented-control/types.ts +22 -22
  287. package/src/select/index.ts +28 -28
  288. package/src/select/select-backdrop.tsx +25 -25
  289. package/src/select/select-content.tsx +49 -49
  290. package/src/select/select-drag-indicator.tsx +19 -19
  291. package/src/select/select-icon.tsx +25 -25
  292. package/src/select/select-input.tsx +32 -32
  293. package/src/select/select-item-text.tsx +30 -30
  294. package/src/select/select-item.tsx +72 -72
  295. package/src/select/select-portal.tsx +22 -22
  296. package/src/select/select-scroll-view.tsx +22 -22
  297. package/src/select/select-trigger.tsx +64 -64
  298. package/src/select/select.tsx +101 -101
  299. package/src/select/styles.ts +114 -114
  300. package/src/select/types.ts +92 -92
  301. package/src/skeleton/index.ts +2 -2
  302. package/src/skeleton/skeleton.tsx +29 -29
  303. package/src/skeleton/styles.ts +14 -14
  304. package/src/skeleton/types.ts +12 -12
  305. package/src/slider/index.ts +12 -12
  306. package/src/slider/slider-filled-track.tsx +31 -31
  307. package/src/slider/slider-thumb.tsx +52 -52
  308. package/src/slider/slider-track.tsx +154 -154
  309. package/src/slider/slider.tsx +193 -193
  310. package/src/slider/styles.ts +71 -71
  311. package/src/slider/types.ts +47 -47
  312. package/src/snackbar/index.ts +2 -2
  313. package/src/snackbar/snackbar.tsx +39 -39
  314. package/src/snackbar/styles.ts +29 -29
  315. package/src/snackbar/types.ts +21 -21
  316. package/src/spinner/index.ts +2 -2
  317. package/src/spinner/spinner.tsx +29 -29
  318. package/src/spinner/styles.ts +15 -15
  319. package/src/spinner/types.ts +10 -10
  320. package/src/stack/index.ts +2 -2
  321. package/src/stack/stack.tsx +49 -49
  322. package/src/stack/styles.ts +25 -25
  323. package/src/stack/types.ts +15 -15
  324. package/src/stat/index.ts +2 -2
  325. package/src/stat/stat.tsx +48 -48
  326. package/src/stat/styles.ts +34 -34
  327. package/src/stat/types.ts +24 -24
  328. package/src/stepper/index.ts +2 -2
  329. package/src/stepper/stepper.tsx +95 -95
  330. package/src/stepper/styles.ts +49 -49
  331. package/src/stepper/types.ts +20 -20
  332. package/src/switch/index.ts +2 -2
  333. package/src/switch/styles.ts +24 -24
  334. package/src/switch/switch.tsx +67 -67
  335. package/src/switch/types.ts +23 -23
  336. package/src/table/index.ts +2 -2
  337. package/src/table/styles.ts +12 -12
  338. package/src/table/table.tsx +52 -52
  339. package/src/table/types.ts +10 -10
  340. package/src/tabs/index.ts +18 -18
  341. package/src/tabs/styles.ts +113 -113
  342. package/src/tabs/tab-list.tsx +29 -29
  343. package/src/tabs/tab-panel.tsx +29 -29
  344. package/src/tabs/tab-panels.tsx +21 -21
  345. package/src/tabs/tab-text.tsx +26 -26
  346. package/src/tabs/tab.tsx +56 -56
  347. package/src/tabs/tabs.tsx +71 -71
  348. package/src/tabs/types.ts +53 -53
  349. package/src/tag/index.ts +14 -14
  350. package/src/tag/styles.ts +115 -115
  351. package/src/tag/tag-close-button.tsx +26 -26
  352. package/src/tag/tag-icon.tsx +20 -20
  353. package/src/tag/tag-text.tsx +22 -22
  354. package/src/tag/tag.tsx +40 -40
  355. package/src/tag/types.ts +33 -33
  356. package/src/tags-input/index.ts +18 -18
  357. package/src/tags-input/styles.ts +29 -29
  358. package/src/tags-input/tags-input.tsx +149 -149
  359. package/src/tags-input/types.ts +37 -37
  360. package/src/text/index.ts +2 -2
  361. package/src/text/styles.ts +54 -54
  362. package/src/text/text.tsx +51 -51
  363. package/src/text/types.ts +36 -36
  364. package/src/textarea/index.ts +2 -2
  365. package/src/textarea/styles.ts +37 -37
  366. package/src/textarea/textarea.tsx +68 -68
  367. package/src/textarea/types.ts +14 -14
  368. package/src/timeline/index.ts +2 -2
  369. package/src/timeline/styles.ts +57 -57
  370. package/src/timeline/timeline.tsx +52 -52
  371. package/src/timeline/types.ts +30 -30
  372. package/src/toast/index.ts +17 -17
  373. package/src/toast/styles.ts +118 -118
  374. package/src/toast/toast-description.tsx +22 -22
  375. package/src/toast/toast-provider.tsx +136 -136
  376. package/src/toast/toast-title.tsx +22 -22
  377. package/src/toast/toast.tsx +43 -43
  378. package/src/toast/types.ts +50 -50
  379. package/src/toast/use-toast.ts +7 -7
  380. package/src/toggle/index.ts +2 -2
  381. package/src/toggle/styles.ts +30 -30
  382. package/src/toggle/toggle.tsx +25 -25
  383. package/src/toggle/types.ts +15 -15
  384. package/src/toggle-group/index.ts +2 -2
  385. package/src/toggle-group/styles.ts +35 -35
  386. package/src/toggle-group/toggle-group.tsx +60 -60
  387. package/src/toggle-group/types.ts +29 -29
  388. package/src/tooltip/index.ts +11 -11
  389. package/src/tooltip/styles.ts +9 -9
  390. package/src/tooltip/tooltip-content.tsx +19 -19
  391. package/src/tooltip/tooltip-text.tsx +19 -19
  392. package/src/tooltip/tooltip.tsx +116 -116
  393. package/src/tooltip/types.ts +35 -35
  394. package/src/utils/brand.ts +5 -5
  395. package/src/utils/create-context.ts +17 -17
  396. package/src/utils/index.ts +8 -8
  397. package/src/utils/types.ts +20 -20
  398. package/src/visually-hidden/index.ts +2 -2
  399. package/src/visually-hidden/styles.ts +6 -6
  400. package/src/visually-hidden/types.ts +6 -6
  401. package/src/visually-hidden/visually-hidden.tsx +22 -22
  402. package/tailwind-preset.js +203 -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';