zkit-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/README.md +267 -0
  2. package/dist/ComponentLibProvider.d.ts +15 -0
  3. package/dist/ComponentLibProvider.d.ts.map +1 -0
  4. package/dist/ComponentLibProvider.js +18 -0
  5. package/dist/assets/animations/spinner.json +1 -0
  6. package/dist/assets/icons/check.svg +4 -0
  7. package/dist/assets/icons/toast/error.webp +0 -0
  8. package/dist/assets/icons/toast/success.webp +0 -0
  9. package/dist/assets/icons/toast/warning.webp +0 -0
  10. package/dist/config.d.ts +16 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +42 -0
  13. package/dist/i18n/I18nProvider.d.ts +11 -0
  14. package/dist/i18n/I18nProvider.d.ts.map +1 -0
  15. package/dist/i18n/I18nProvider.js +84 -0
  16. package/dist/i18n/locale.d.ts +7 -0
  17. package/dist/i18n/locale.d.ts.map +1 -0
  18. package/dist/i18n/locale.js +87 -0
  19. package/dist/i18n/locales/de.d.ts +51 -0
  20. package/dist/i18n/locales/de.d.ts.map +1 -0
  21. package/dist/i18n/locales/de.js +62 -0
  22. package/dist/i18n/locales/en-US.d.ts +51 -0
  23. package/dist/i18n/locales/en-US.d.ts.map +1 -0
  24. package/dist/i18n/locales/en-US.js +62 -0
  25. package/dist/i18n/locales/index.d.ts +9 -0
  26. package/dist/i18n/locales/index.d.ts.map +1 -0
  27. package/dist/i18n/locales/index.js +23 -0
  28. package/dist/i18n/locales/ja.d.ts +51 -0
  29. package/dist/i18n/locales/ja.d.ts.map +1 -0
  30. package/dist/i18n/locales/ja.js +62 -0
  31. package/dist/i18n/locales/zh-CN.d.ts +51 -0
  32. package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
  33. package/dist/i18n/locales/zh-CN.js +62 -0
  34. package/dist/i18n/locales/zh-TW.d.ts +51 -0
  35. package/dist/i18n/locales/zh-TW.d.ts.map +1 -0
  36. package/dist/i18n/locales/zh-TW.js +62 -0
  37. package/dist/i18n/types.d.ts +11 -0
  38. package/dist/i18n/types.d.ts.map +1 -0
  39. package/dist/i18n/types.js +2 -0
  40. package/dist/i18n/useI18n.d.ts +2 -0
  41. package/dist/i18n/useI18n.d.ts.map +1 -0
  42. package/dist/i18n/useI18n.js +41 -0
  43. package/dist/index.d.ts +57 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +90 -0
  46. package/dist/services/ActionDialogService/ActionDialog.d.ts +4 -0
  47. package/dist/services/ActionDialogService/ActionDialog.d.ts.map +1 -0
  48. package/dist/services/ActionDialogService/ActionDialog.js +531 -0
  49. package/dist/services/ActionDialogService/index.d.ts +4 -0
  50. package/dist/services/ActionDialogService/index.d.ts.map +1 -0
  51. package/dist/services/ActionDialogService/index.js +8 -0
  52. package/dist/services/ActionDialogService/service.d.ts +7 -0
  53. package/dist/services/ActionDialogService/service.d.ts.map +1 -0
  54. package/dist/services/ActionDialogService/service.js +309 -0
  55. package/dist/services/ActionDialogService/shared.d.ts +17 -0
  56. package/dist/services/ActionDialogService/shared.d.ts.map +1 -0
  57. package/dist/services/ActionDialogService/shared.js +167 -0
  58. package/dist/services/ActionDialogService/types.d.ts +160 -0
  59. package/dist/services/ActionDialogService/types.d.ts.map +1 -0
  60. package/dist/services/ActionDialogService/types.js +2 -0
  61. package/dist/services/CardToastService/index.d.ts +115 -0
  62. package/dist/services/CardToastService/index.d.ts.map +1 -0
  63. package/dist/services/CardToastService/index.js +899 -0
  64. package/dist/services/ImagePreviewService/index.d.ts +179 -0
  65. package/dist/services/ImagePreviewService/index.d.ts.map +1 -0
  66. package/dist/services/ImagePreviewService/index.js +1383 -0
  67. package/dist/services/LoadingService/index.d.ts +146 -0
  68. package/dist/services/LoadingService/index.d.ts.map +1 -0
  69. package/dist/services/LoadingService/index.js +827 -0
  70. package/dist/services/PermissionPurposeDialogService/index.d.ts +99 -0
  71. package/dist/services/PermissionPurposeDialogService/index.d.ts.map +1 -0
  72. package/dist/services/PermissionPurposeDialogService/index.js +360 -0
  73. package/dist/services/PickerService/index.d.ts +128 -0
  74. package/dist/services/PickerService/index.d.ts.map +1 -0
  75. package/dist/services/PickerService/index.js +250 -0
  76. package/dist/theme/ThemeProvider.d.ts +15 -0
  77. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  78. package/dist/theme/ThemeProvider.js +61 -0
  79. package/dist/theme/defaultTheme.d.ts +3 -0
  80. package/dist/theme/defaultTheme.d.ts.map +1 -0
  81. package/dist/theme/defaultTheme.js +28 -0
  82. package/dist/theme/mergeTheme.d.ts +3 -0
  83. package/dist/theme/mergeTheme.d.ts.map +1 -0
  84. package/dist/theme/mergeTheme.js +19 -0
  85. package/dist/theme/types.d.ts +17 -0
  86. package/dist/theme/types.d.ts.map +1 -0
  87. package/dist/theme/types.js +2 -0
  88. package/dist/theme/useTheme.d.ts +2 -0
  89. package/dist/theme/useTheme.d.ts.map +1 -0
  90. package/dist/theme/useTheme.js +46 -0
  91. package/dist/ui/Accordion/index.d.ts +99 -0
  92. package/dist/ui/Accordion/index.d.ts.map +1 -0
  93. package/dist/ui/Accordion/index.js +777 -0
  94. package/dist/ui/AddressCascader/index.d.ts +58 -0
  95. package/dist/ui/AddressCascader/index.d.ts.map +1 -0
  96. package/dist/ui/AddressCascader/index.js +710 -0
  97. package/dist/ui/BetweenTime/index.d.ts +82 -0
  98. package/dist/ui/BetweenTime/index.d.ts.map +1 -0
  99. package/dist/ui/BetweenTime/index.js +702 -0
  100. package/dist/ui/Button/index.d.ts +93 -0
  101. package/dist/ui/Button/index.d.ts.map +1 -0
  102. package/dist/ui/Button/index.js +832 -0
  103. package/dist/ui/Checkbox/index.d.ts +103 -0
  104. package/dist/ui/Checkbox/index.d.ts.map +1 -0
  105. package/dist/ui/Checkbox/index.js +694 -0
  106. package/dist/ui/DatePicker/index.d.ts +5 -0
  107. package/dist/ui/DatePicker/index.d.ts.map +1 -0
  108. package/dist/ui/DatePicker/index.js +157 -0
  109. package/dist/ui/DatePicker/types.d.ts +99 -0
  110. package/dist/ui/DatePicker/types.d.ts.map +1 -0
  111. package/dist/ui/DatePicker/types.js +2 -0
  112. package/dist/ui/DatePicker/utils.d.ts +32 -0
  113. package/dist/ui/DatePicker/utils.d.ts.map +1 -0
  114. package/dist/ui/DatePicker/utils.js +316 -0
  115. package/dist/ui/LinkedScroll/index.d.ts +78 -0
  116. package/dist/ui/LinkedScroll/index.d.ts.map +1 -0
  117. package/dist/ui/LinkedScroll/index.js +470 -0
  118. package/dist/ui/LoadingSpinner/index.d.ts +8 -0
  119. package/dist/ui/LoadingSpinner/index.d.ts.map +1 -0
  120. package/dist/ui/LoadingSpinner/index.js +122 -0
  121. package/dist/ui/Picker/actionBar.d.ts +14 -0
  122. package/dist/ui/Picker/actionBar.d.ts.map +1 -0
  123. package/dist/ui/Picker/actionBar.js +28 -0
  124. package/dist/ui/Picker/index.d.ts +5 -0
  125. package/dist/ui/Picker/index.d.ts.map +1 -0
  126. package/dist/ui/Picker/index.js +412 -0
  127. package/dist/ui/Picker/types.d.ts +80 -0
  128. package/dist/ui/Picker/types.d.ts.map +1 -0
  129. package/dist/ui/Picker/types.js +2 -0
  130. package/dist/ui/Picker/utils.d.ts +27 -0
  131. package/dist/ui/Picker/utils.d.ts.map +1 -0
  132. package/dist/ui/Picker/utils.js +182 -0
  133. package/dist/ui/Radio/index.d.ts +95 -0
  134. package/dist/ui/Radio/index.d.ts.map +1 -0
  135. package/dist/ui/Radio/index.js +633 -0
  136. package/dist/ui/Sheet/index.d.ts +12 -0
  137. package/dist/ui/Sheet/index.d.ts.map +1 -0
  138. package/dist/ui/Sheet/index.js +832 -0
  139. package/dist/ui/Sheet/nativeBottom.d.ts +52 -0
  140. package/dist/ui/Sheet/nativeBottom.d.ts.map +1 -0
  141. package/dist/ui/Sheet/nativeBottom.js +442 -0
  142. package/dist/ui/Sheet/nativeBottom.web.d.ts +18 -0
  143. package/dist/ui/Sheet/nativeBottom.web.d.ts.map +1 -0
  144. package/dist/ui/Sheet/nativeBottom.web.js +46 -0
  145. package/dist/ui/Sheet/slots.d.ts +5 -0
  146. package/dist/ui/Sheet/slots.d.ts.map +1 -0
  147. package/dist/ui/Sheet/slots.js +40 -0
  148. package/dist/ui/Sheet/types.d.ts +131 -0
  149. package/dist/ui/Sheet/types.d.ts.map +1 -0
  150. package/dist/ui/Sheet/types.js +2 -0
  151. package/dist/ui/SliderCaptcha/index.d.ts +57 -0
  152. package/dist/ui/SliderCaptcha/index.d.ts.map +1 -0
  153. package/dist/ui/SliderCaptcha/index.js +586 -0
  154. package/dist/ui/Switch/index.d.ts +72 -0
  155. package/dist/ui/Switch/index.d.ts.map +1 -0
  156. package/dist/ui/Switch/index.js +671 -0
  157. package/dist/ui/Text/index.d.ts +41 -0
  158. package/dist/ui/Text/index.d.ts.map +1 -0
  159. package/dist/ui/Text/index.js +325 -0
  160. package/dist/ui/TextInput/TextInputPrimitive.d.ts +8 -0
  161. package/dist/ui/TextInput/TextInputPrimitive.d.ts.map +1 -0
  162. package/dist/ui/TextInput/TextInputPrimitive.js +54 -0
  163. package/dist/ui/TextInput/index.d.ts +89 -0
  164. package/dist/ui/TextInput/index.d.ts.map +1 -0
  165. package/dist/ui/TextInput/index.js +545 -0
  166. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.d.ts +39 -0
  167. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.d.ts.map +1 -0
  168. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.js +60 -0
  169. package/dist/ui/WheelColumn/constants.d.ts +2 -0
  170. package/dist/ui/WheelColumn/constants.d.ts.map +1 -0
  171. package/dist/ui/WheelColumn/constants.js +4 -0
  172. package/dist/ui/WheelColumn/index.d.ts +59 -0
  173. package/dist/ui/WheelColumn/index.d.ts.map +1 -0
  174. package/dist/ui/WheelColumn/index.js +547 -0
  175. package/ios/ZKitWheelPicker.h +18 -0
  176. package/ios/ZKitWheelPicker.mm +230 -0
  177. package/ios/ZKitWheelPickerLabel.h +10 -0
  178. package/ios/ZKitWheelPickerLabel.mm +31 -0
  179. package/ios/ZKitWheelPickerManager.h +5 -0
  180. package/ios/ZKitWheelPickerManager.mm +53 -0
  181. package/package.json +75 -0
  182. package/react-native.config.js +10 -0
  183. package/src/ComponentLibProvider.tsx +56 -0
  184. package/src/assets/animations/spinner.json +1 -0
  185. package/src/assets/icons/check.svg +4 -0
  186. package/src/assets/icons/toast/error.webp +0 -0
  187. package/src/assets/icons/toast/success.webp +0 -0
  188. package/src/assets/icons/toast/warning.webp +0 -0
  189. package/src/config.ts +65 -0
  190. package/src/i18n/I18nProvider.tsx +69 -0
  191. package/src/i18n/locale.ts +108 -0
  192. package/src/i18n/locales/de.ts +67 -0
  193. package/src/i18n/locales/en-US.ts +67 -0
  194. package/src/i18n/locales/index.ts +19 -0
  195. package/src/i18n/locales/ja.ts +67 -0
  196. package/src/i18n/locales/zh-CN.ts +67 -0
  197. package/src/i18n/locales/zh-TW.ts +67 -0
  198. package/src/i18n/types.ts +21 -0
  199. package/src/i18n/useI18n.ts +7 -0
  200. package/src/index.ts +395 -0
  201. package/src/services/ActionDialogService/ActionDialog.tsx +756 -0
  202. package/src/services/ActionDialogService/README.md +146 -0
  203. package/src/services/ActionDialogService/index.tsx +39 -0
  204. package/src/services/ActionDialogService/service.tsx +355 -0
  205. package/src/services/ActionDialogService/shared.ts +204 -0
  206. package/src/services/ActionDialogService/types.ts +197 -0
  207. package/src/services/CardToastService/README.md +128 -0
  208. package/src/services/CardToastService/index.tsx +1284 -0
  209. package/src/services/ImagePreviewService/README.md +67 -0
  210. package/src/services/ImagePreviewService/index.tsx +2148 -0
  211. package/src/services/LoadingService/README.md +128 -0
  212. package/src/services/LoadingService/index.tsx +1113 -0
  213. package/src/services/PermissionPurposeDialogService/README.md +80 -0
  214. package/src/services/PermissionPurposeDialogService/index.tsx +449 -0
  215. package/src/services/PickerService/README.md +154 -0
  216. package/src/services/PickerService/index.tsx +410 -0
  217. package/src/theme/ThemeProvider.tsx +40 -0
  218. package/src/theme/defaultTheme.ts +30 -0
  219. package/src/theme/mergeTheme.ts +16 -0
  220. package/src/theme/types.ts +36 -0
  221. package/src/theme/useTheme.ts +10 -0
  222. package/src/types/svg.d.ts +9 -0
  223. package/src/types/vant-area-data.d.ts +19 -0
  224. package/src/ui/Accordion/README.md +67 -0
  225. package/src/ui/Accordion/index.tsx +1221 -0
  226. package/src/ui/AddressCascader/README.md +144 -0
  227. package/src/ui/AddressCascader/index.tsx +1084 -0
  228. package/src/ui/BetweenTime/README.md +490 -0
  229. package/src/ui/BetweenTime/index.tsx +1001 -0
  230. package/src/ui/Button/README.md +186 -0
  231. package/src/ui/Button/index.tsx +1356 -0
  232. package/src/ui/Checkbox/README.md +211 -0
  233. package/src/ui/Checkbox/index.tsx +1231 -0
  234. package/src/ui/DatePicker/README.md +117 -0
  235. package/src/ui/DatePicker/index.tsx +295 -0
  236. package/src/ui/DatePicker/types.ts +115 -0
  237. package/src/ui/DatePicker/utils.ts +405 -0
  238. package/src/ui/LinkedScroll/README.md +55 -0
  239. package/src/ui/LinkedScroll/index.tsx +809 -0
  240. package/src/ui/LoadingSpinner/index.tsx +104 -0
  241. package/src/ui/Picker/README.md +190 -0
  242. package/src/ui/Picker/actionBar.tsx +70 -0
  243. package/src/ui/Picker/index.tsx +694 -0
  244. package/src/ui/Picker/types.ts +94 -0
  245. package/src/ui/Picker/utils.ts +239 -0
  246. package/src/ui/Radio/README.md +207 -0
  247. package/src/ui/Radio/index.tsx +1117 -0
  248. package/src/ui/Sheet/README.md +82 -0
  249. package/src/ui/Sheet/index.tsx +1163 -0
  250. package/src/ui/Sheet/nativeBottom.tsx +683 -0
  251. package/src/ui/Sheet/nativeBottom.web.tsx +40 -0
  252. package/src/ui/Sheet/slots.tsx +75 -0
  253. package/src/ui/Sheet/types.ts +195 -0
  254. package/src/ui/SliderCaptcha/README.md +61 -0
  255. package/src/ui/SliderCaptcha/index.tsx +816 -0
  256. package/src/ui/Switch/README.md +143 -0
  257. package/src/ui/Switch/index.tsx +1092 -0
  258. package/src/ui/Text/README.md +157 -0
  259. package/src/ui/Text/index.tsx +450 -0
  260. package/src/ui/TextInput/README.md +197 -0
  261. package/src/ui/TextInput/TextInputPrimitive.tsx +44 -0
  262. package/src/ui/TextInput/index.tsx +942 -0
  263. package/src/ui/WheelColumn/README.md +104 -0
  264. package/src/ui/WheelColumn/ZKitWheelPickerNativeComponent.tsx +68 -0
  265. package/src/ui/WheelColumn/constants.ts +1 -0
  266. package/src/ui/WheelColumn/index.tsx +826 -0
  267. package/zkit-ui.podspec +19 -0
@@ -0,0 +1,826 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Platform,
4
+ StyleSheet,
5
+ View,
6
+ type AccessibilityActionEvent,
7
+ type ColorValue,
8
+ type StyleProp,
9
+ type TextStyle,
10
+ type ViewProps,
11
+ type ViewStyle,
12
+ } from 'react-native';
13
+ import Animated, {
14
+ cancelAnimation,
15
+ Easing,
16
+ useAnimatedStyle,
17
+ useSharedValue,
18
+ withTiming,
19
+ } from 'react-native-reanimated';
20
+ import { scheduleOnRN } from 'react-native-worklets';
21
+ import { getMaxFontSizeMultiplier, sp, wp } from 'zkit-tools';
22
+ import { useTheme } from '../../theme/useTheme';
23
+ import {
24
+ ZKitWheelPicker,
25
+ syncZKitWheelPickerCurrentSelection,
26
+ type ZKitWheelPickerChangeEvent,
27
+ } from './ZKitWheelPickerNativeComponent';
28
+ export { WHEEL_SELECTION_BACKGROUND_COLOR } from './constants';
29
+
30
+ export const WHEEL_VISIBLE_ITEMS = 5;
31
+
32
+ // iOS 走系统 UIPickerView,Android/Web 走自绘 transform 路径。
33
+ // 这些尺寸是当前 app 端多轮调校后的稳定观感,保留为默认公共规格。
34
+ const IOS_CONFIRM_SYNC_TIMEOUT_MS = 64;
35
+ const IOS_NATIVE_PICKER_HEIGHT = wp(260);
36
+ const IOS_NATIVE_PICKER_ROW_HEIGHT = wp(50);
37
+ const IOS_NATIVE_PICKER_FONT_SIZE = sp(22);
38
+ const BASE_WHEEL_ITEM_HEIGHT = wp(44);
39
+ const BASE_WHEEL_AREA_HEIGHT = BASE_WHEEL_ITEM_HEIGHT * WHEEL_VISIBLE_ITEMS;
40
+
41
+ export const WHEEL_VIEWPORT_HEIGHT = Platform.OS === 'ios' ? IOS_NATIVE_PICKER_HEIGHT : BASE_WHEEL_AREA_HEIGHT;
42
+ export const WHEEL_AREA_HEIGHT =
43
+ Platform.OS === 'ios' ? Math.max(IOS_NATIVE_PICKER_HEIGHT, BASE_WHEEL_AREA_HEIGHT) : BASE_WHEEL_AREA_HEIGHT;
44
+ export const WHEEL_AREA_VERTICAL_INSET = Math.max(0, WHEEL_AREA_HEIGHT - WHEEL_VIEWPORT_HEIGHT) / 2;
45
+ export const WHEEL_ITEM_HEIGHT =
46
+ Platform.OS === 'ios' ? IOS_NATIVE_PICKER_HEIGHT / WHEEL_VISIBLE_ITEMS : BASE_WHEEL_ITEM_HEIGHT;
47
+
48
+ const CENTER_OFFSET = WHEEL_ITEM_HEIGHT * Math.floor(WHEEL_VISIBLE_ITEMS / 2);
49
+ const IOS_SETTLE_DELAY_MS = 90;
50
+
51
+ const SNAP_DURATION_MIN = 140;
52
+ const SNAP_DURATION_MAX = 280;
53
+ const SNAP_DURATION_PER_ITEM = 24;
54
+
55
+ const RELEASE_VELOCITY_DEADZONE = 220;
56
+ const RELEASE_LOCK_DISTANCE_ITEMS = 0.1;
57
+ const RELEASE_LOCK_MAX_VELOCITY = 380;
58
+ const MAX_FLING_ITEMS = 7;
59
+ const ANDROID_VELOCITY_WINDOW_MS = 90;
60
+
61
+ const SNAP_EASING = Easing.bezier(0.22, 1, 0.36, 1);
62
+
63
+ export type WheelColumnValue = string | number;
64
+
65
+ export type WheelColumnOption<TValue extends WheelColumnValue = WheelColumnValue> = {
66
+ value: TValue;
67
+ label: string;
68
+ disabled?: boolean;
69
+ key?: React.Key;
70
+ testID?: string;
71
+ accessibilityLabel?: string;
72
+ };
73
+
74
+ export type WheelColumnChangeSource = 'user' | 'accessibility';
75
+
76
+ export type WheelColumnChangePayload<TValue extends WheelColumnValue = WheelColumnValue> = {
77
+ value: TValue;
78
+ index: number;
79
+ option: WheelColumnOption<TValue>;
80
+ source: WheelColumnChangeSource;
81
+ };
82
+
83
+ export type WheelColumnHandle = {
84
+ scrollToIndex: (index: number, animated?: boolean) => void;
85
+ scrollToValue: (value: WheelColumnValue, animated?: boolean) => void;
86
+ settleToNearest: (animated?: boolean) => number;
87
+ syncCurrentSelection: () => Promise<number>;
88
+ };
89
+
90
+ type NativeViewProps = Omit<ViewProps, 'children' | 'style' | 'onChange'>;
91
+
92
+ export type WheelColumnProps<TValue extends WheelColumnValue = WheelColumnValue> = NativeViewProps & {
93
+ options: WheelColumnOption<TValue>[];
94
+ value?: TValue | null;
95
+ defaultValue?: TValue | null;
96
+ onChange?: (payload: WheelColumnChangePayload<TValue>) => void;
97
+
98
+ disabled?: boolean;
99
+ width?: number;
100
+
101
+ style?: StyleProp<ViewStyle>;
102
+ itemTextStyle?: StyleProp<TextStyle>;
103
+ selectedItemTextStyle?: StyleProp<TextStyle>;
104
+ disabledItemTextStyle?: StyleProp<TextStyle>;
105
+ numberOfLines?: number;
106
+ };
107
+
108
+ // 简短别名保留给 Picker / Date 类内部数据构造使用。
109
+ export type WheelOption<TValue extends WheelColumnValue = WheelColumnValue> = WheelColumnOption<TValue>;
110
+
111
+ function clampNumber(n: number, min: number, max: number) {
112
+ 'worklet';
113
+ return Math.max(min, Math.min(max, n));
114
+ }
115
+
116
+ function indexToOffset(index: number) {
117
+ 'worklet';
118
+ return index * WHEEL_ITEM_HEIGHT;
119
+ }
120
+
121
+ function getReleaseDeltaItems(velocityY: number) {
122
+ const speed = Math.abs(velocityY);
123
+ if (speed < RELEASE_VELOCITY_DEADZONE) return 0;
124
+ const projected = Math.pow(speed / 1400, 1.05) * 3.6;
125
+ return clampNumber(projected, 0, MAX_FLING_ITEMS);
126
+ }
127
+
128
+ function getTargetIndexFromRelease(offset: number, velocityY: number, maxIndex: number) {
129
+ const currentIndexFloat = offset / WHEEL_ITEM_HEIGHT;
130
+ const nearestIndex = Math.round(currentIndexFloat);
131
+ const distanceToNearest = Math.abs(currentIndexFloat - nearestIndex);
132
+ const speed = Math.abs(velocityY);
133
+
134
+ if (speed < RELEASE_VELOCITY_DEADZONE) {
135
+ return clampNumber(nearestIndex, 0, maxIndex);
136
+ }
137
+
138
+ if (distanceToNearest < RELEASE_LOCK_DISTANCE_ITEMS && speed < RELEASE_LOCK_MAX_VELOCITY) {
139
+ return clampNumber(nearestIndex, 0, maxIndex);
140
+ }
141
+
142
+ const deltaItems = getReleaseDeltaItems(velocityY);
143
+ if (deltaItems <= 0) {
144
+ return clampNumber(nearestIndex, 0, maxIndex);
145
+ }
146
+
147
+ const direction = velocityY < 0 ? 1 : -1;
148
+ let targetIndex = Math.round(currentIndexFloat + direction * deltaItems);
149
+
150
+ if (targetIndex === nearestIndex) {
151
+ targetIndex = nearestIndex + direction;
152
+ }
153
+
154
+ return clampNumber(targetIndex, 0, maxIndex);
155
+ }
156
+
157
+ function getSnapDuration(fromOffset: number, toOffset: number) {
158
+ const distanceItems = Math.abs(toOffset - fromOffset) / WHEEL_ITEM_HEIGHT;
159
+ return clampNumber(
160
+ Math.round(SNAP_DURATION_MIN + distanceItems * SNAP_DURATION_PER_ITEM),
161
+ SNAP_DURATION_MIN,
162
+ SNAP_DURATION_MAX
163
+ );
164
+ }
165
+
166
+ function findOptionIndex<TValue extends WheelColumnValue>(
167
+ options: readonly WheelColumnOption<TValue>[],
168
+ value: TValue | WheelColumnValue | null | undefined
169
+ ) {
170
+ if (value == null) return -1;
171
+ return options.findIndex((option) => Object.is(option.value, value));
172
+ }
173
+
174
+ function findNearestEnabledIndex<TValue extends WheelColumnValue>(
175
+ options: readonly WheelColumnOption<TValue>[],
176
+ index: number,
177
+ direction = 0
178
+ ) {
179
+ if (!options.length) return -1;
180
+
181
+ const clampedIndex = clampNumber(Math.round(index), 0, options.length - 1);
182
+ if (!options[clampedIndex]?.disabled) return clampedIndex;
183
+
184
+ const walkForward = () => {
185
+ for (let i = clampedIndex + 1; i < options.length; i += 1) {
186
+ if (!options[i]?.disabled) return i;
187
+ }
188
+ return -1;
189
+ };
190
+
191
+ const walkBackward = () => {
192
+ for (let i = clampedIndex - 1; i >= 0; i -= 1) {
193
+ if (!options[i]?.disabled) return i;
194
+ }
195
+ return -1;
196
+ };
197
+
198
+ if (direction > 0) {
199
+ const next = walkForward();
200
+ return next >= 0 ? next : walkBackward();
201
+ }
202
+
203
+ if (direction < 0) {
204
+ const previous = walkBackward();
205
+ return previous >= 0 ? previous : walkForward();
206
+ }
207
+
208
+ for (let distance = 1; distance < options.length; distance += 1) {
209
+ const previous = clampedIndex - distance;
210
+ if (previous >= 0 && !options[previous]?.disabled) return previous;
211
+
212
+ const next = clampedIndex + distance;
213
+ if (next < options.length && !options[next]?.disabled) return next;
214
+ }
215
+
216
+ return -1;
217
+ }
218
+
219
+ function resolveDisplayIndex<TValue extends WheelColumnValue>(
220
+ options: readonly WheelColumnOption<TValue>[],
221
+ value: TValue | null | undefined
222
+ ) {
223
+ if (!options.length) return 0;
224
+
225
+ const selectedIndex = findOptionIndex(options, value);
226
+ if (selectedIndex >= 0) return selectedIndex;
227
+
228
+ const firstEnabledIndex = findNearestEnabledIndex(options, 0);
229
+ return firstEnabledIndex >= 0 ? firstEnabledIndex : 0;
230
+ }
231
+
232
+ function normalizeFontWeight(fontWeight: TextStyle['fontWeight']) {
233
+ if (fontWeight == null) return undefined;
234
+ return String(fontWeight);
235
+ }
236
+
237
+ function normalizeFontStyle(fontStyle: TextStyle['fontStyle']) {
238
+ if (fontStyle == null) return undefined;
239
+ return fontStyle;
240
+ }
241
+
242
+ function optionKey(option: WheelColumnOption, index: number) {
243
+ return option.key ?? option.value ?? index;
244
+ }
245
+
246
+ function resolveWebInteractionStyle(disabled: boolean): StyleProp<ViewStyle> {
247
+ if (Platform.OS !== 'web') return undefined;
248
+ return {
249
+ cursor: disabled ? 'not-allowed' : 'grab',
250
+ touchAction: 'none',
251
+ userSelect: 'none',
252
+ } as unknown as ViewStyle;
253
+ }
254
+
255
+ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(function WheelColumn(
256
+ {
257
+ options,
258
+ value,
259
+ defaultValue,
260
+ onChange,
261
+ disabled = false,
262
+ width,
263
+ style,
264
+ itemTextStyle,
265
+ selectedItemTextStyle,
266
+ disabledItemTextStyle,
267
+ numberOfLines = 1,
268
+ accessibilityLabel,
269
+ accessibilityHint,
270
+ accessibilityState,
271
+ testID,
272
+ ...viewProps
273
+ },
274
+ ref
275
+ ) {
276
+ const theme = useTheme();
277
+ const isControlled = value !== undefined;
278
+ const firstDefaultValue = React.useMemo(() => {
279
+ if (defaultValue != null && findOptionIndex(options, defaultValue) >= 0) return defaultValue;
280
+
281
+ const firstEnabledIndex = findNearestEnabledIndex(options, 0);
282
+ if (firstEnabledIndex >= 0) return options[firstEnabledIndex]?.value ?? null;
283
+ return options[0]?.value ?? null;
284
+ }, [defaultValue, options]);
285
+ const [uncontrolledValue, setUncontrolledValue] = React.useState<WheelColumnValue | null>(firstDefaultValue);
286
+ const resolvedValue = isControlled ? value : uncontrolledValue;
287
+ const maxIndex = Math.max(0, options.length - 1);
288
+ const maxOffset = indexToOffset(maxIndex);
289
+ const displayIndex = resolveDisplayIndex(options, resolvedValue);
290
+ const offsetY = useSharedValue(indexToOffset(displayIndex));
291
+ const dragStartOffset = useSharedValue(indexToOffset(displayIndex));
292
+ const isUserInteracting = useSharedValue(false);
293
+
294
+ const iosPickerRef = React.useRef<unknown>(null);
295
+ const iosSelectedIndexRef = React.useRef(displayIndex);
296
+ const iosSettleTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
297
+ const iosSyncTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
298
+ const iosSyncResolverRef = React.useRef<((index: number) => void) | null>(null);
299
+ const [iosSelectedIndex, setIosSelectedIndex] = React.useState(displayIndex);
300
+
301
+ const selectedValueRef = React.useRef<WheelColumnValue | null | undefined>(resolvedValue);
302
+ const displayIndexRef = React.useRef(displayIndex);
303
+
304
+ const flattenedItemTextStyle = React.useMemo(() => StyleSheet.flatten(itemTextStyle) ?? {}, [itemTextStyle]);
305
+ const flattenedSelectedItemTextStyle = React.useMemo(
306
+ () => StyleSheet.flatten(selectedItemTextStyle) ?? {},
307
+ [selectedItemTextStyle]
308
+ );
309
+ const flattenedDisabledItemTextStyle = React.useMemo(
310
+ () => StyleSheet.flatten(disabledItemTextStyle) ?? {},
311
+ [disabledItemTextStyle]
312
+ );
313
+
314
+ const selectedTextColor = (flattenedSelectedItemTextStyle.color ??
315
+ flattenedItemTextStyle.color ??
316
+ theme.colors.onSurface) as ColorValue;
317
+ const itemTextColor = (flattenedItemTextStyle.color ?? theme.colors.muted) as ColorValue;
318
+ const disabledTextColor = (flattenedDisabledItemTextStyle.color ?? theme.colors.disabled) as ColorValue;
319
+ const nativeFontSize =
320
+ typeof flattenedSelectedItemTextStyle.fontSize === 'number'
321
+ ? flattenedSelectedItemTextStyle.fontSize
322
+ : typeof flattenedItemTextStyle.fontSize === 'number'
323
+ ? flattenedItemTextStyle.fontSize
324
+ : IOS_NATIVE_PICKER_FONT_SIZE;
325
+ const nativeFontWeight =
326
+ normalizeFontWeight(flattenedSelectedItemTextStyle.fontWeight) ??
327
+ normalizeFontWeight(flattenedItemTextStyle.fontWeight) ??
328
+ '500';
329
+ const nativeFontFamily = flattenedSelectedItemTextStyle.fontFamily ?? flattenedItemTextStyle.fontFamily;
330
+ const nativeFontStyle =
331
+ normalizeFontStyle(flattenedSelectedItemTextStyle.fontStyle) ??
332
+ normalizeFontStyle(flattenedItemTextStyle.fontStyle);
333
+
334
+ const clearIOSSettleTimer = React.useCallback(() => {
335
+ if (iosSettleTimerRef.current != null) {
336
+ clearTimeout(iosSettleTimerRef.current);
337
+ iosSettleTimerRef.current = null;
338
+ }
339
+ }, []);
340
+
341
+ const clearIOSSyncTimer = React.useCallback(() => {
342
+ if (iosSyncTimerRef.current != null) {
343
+ clearTimeout(iosSyncTimerRef.current);
344
+ iosSyncTimerRef.current = null;
345
+ }
346
+ }, []);
347
+
348
+ const resolveSelectableIndex = React.useCallback(
349
+ (index: number, direction = 0) => {
350
+ const next = findNearestEnabledIndex(options, index, direction);
351
+ return next >= 0 ? next : clampNumber(Math.round(index), 0, maxIndex);
352
+ },
353
+ [maxIndex, options]
354
+ );
355
+
356
+ const resolveIOSSyncRequest = React.useCallback(
357
+ (index: number) => {
358
+ const resolver = iosSyncResolverRef.current;
359
+ if (!resolver) return;
360
+ iosSyncResolverRef.current = null;
361
+ clearIOSSyncTimer();
362
+ resolver(resolveSelectableIndex(index));
363
+ },
364
+ [clearIOSSyncTimer, resolveSelectableIndex]
365
+ );
366
+
367
+ const syncIOSSelectedIndex = React.useCallback(
368
+ (index: number) => {
369
+ const nextIndex = clampNumber(index, 0, maxIndex);
370
+ iosSelectedIndexRef.current = nextIndex;
371
+ setIosSelectedIndex((previous) => (previous === nextIndex ? previous : nextIndex));
372
+ return nextIndex;
373
+ },
374
+ [maxIndex]
375
+ );
376
+
377
+ const emitSelectedIndex = React.useCallback(
378
+ (index: number, source: WheelColumnChangeSource) => {
379
+ if (!options.length) return;
380
+
381
+ const nextIndex = resolveSelectableIndex(index, index - displayIndexRef.current);
382
+ const option = options[nextIndex];
383
+ if (!option || option.disabled) return;
384
+
385
+ const sameAsCurrent =
386
+ displayIndexRef.current === nextIndex && Object.is(selectedValueRef.current, option.value);
387
+
388
+ if (sameAsCurrent) return;
389
+
390
+ selectedValueRef.current = option.value;
391
+ displayIndexRef.current = nextIndex;
392
+
393
+ if (!isControlled) {
394
+ setUncontrolledValue(option.value);
395
+ }
396
+
397
+ onChange?.({
398
+ value: option.value,
399
+ index: nextIndex,
400
+ option,
401
+ source,
402
+ });
403
+ },
404
+ [isControlled, onChange, options, resolveSelectableIndex]
405
+ );
406
+
407
+ const scheduleIOSSettle = React.useCallback(
408
+ (index: number) => {
409
+ clearIOSSettleTimer();
410
+ iosSettleTimerRef.current = setTimeout(() => {
411
+ iosSettleTimerRef.current = null;
412
+ const nextIndex = resolveSelectableIndex(index, index - displayIndexRef.current);
413
+ if (nextIndex !== index) {
414
+ syncIOSSelectedIndex(nextIndex);
415
+ }
416
+ emitSelectedIndex(nextIndex, 'user');
417
+ }, IOS_SETTLE_DELAY_MS);
418
+ },
419
+ [clearIOSSettleTimer, emitSelectedIndex, resolveSelectableIndex, syncIOSSelectedIndex]
420
+ );
421
+
422
+ const scrollToIndex = React.useCallback(
423
+ (index: number, animated = false) => {
424
+ const nextIndex = clampNumber(index, 0, maxIndex);
425
+ if (Platform.OS === 'ios') {
426
+ clearIOSSettleTimer();
427
+ syncIOSSelectedIndex(nextIndex);
428
+ return;
429
+ }
430
+
431
+ const nextOffset = indexToOffset(nextIndex);
432
+ cancelAnimation(offsetY);
433
+ isUserInteracting.value = false;
434
+ if (animated) {
435
+ offsetY.value = withTiming(nextOffset, {
436
+ duration: getSnapDuration(offsetY.value, nextOffset),
437
+ easing: SNAP_EASING,
438
+ });
439
+ return;
440
+ }
441
+ offsetY.value = nextOffset;
442
+ },
443
+ [clearIOSSettleTimer, isUserInteracting, maxIndex, offsetY, syncIOSSelectedIndex]
444
+ );
445
+
446
+ const scrollToValue = React.useCallback(
447
+ (nextValue: WheelColumnValue, animated = false) => {
448
+ const index = findOptionIndex(options, nextValue);
449
+ if (index < 0) return;
450
+ scrollToIndex(index, animated);
451
+ },
452
+ [options, scrollToIndex]
453
+ );
454
+
455
+ const settleToNearest = React.useCallback(
456
+ (animated = false) => {
457
+ const rawIndex =
458
+ Platform.OS === 'ios'
459
+ ? iosSelectedIndexRef.current
460
+ : Math.round(clampNumber(offsetY.value, 0, maxOffset) / WHEEL_ITEM_HEIGHT);
461
+ const nextIndex = resolveSelectableIndex(rawIndex, rawIndex - displayIndexRef.current);
462
+ scrollToIndex(nextIndex, animated);
463
+ return nextIndex;
464
+ },
465
+ [maxOffset, offsetY, resolveSelectableIndex, scrollToIndex]
466
+ );
467
+
468
+ const syncCurrentSelection = React.useCallback(() => {
469
+ if (Platform.OS !== 'ios') {
470
+ return Promise.resolve(settleToNearest(false));
471
+ }
472
+
473
+ clearIOSSettleTimer();
474
+ resolveIOSSyncRequest(iosSelectedIndexRef.current);
475
+
476
+ return new Promise<number>((resolve) => {
477
+ iosSyncResolverRef.current = resolve;
478
+
479
+ if (iosPickerRef.current != null && syncZKitWheelPickerCurrentSelection(iosPickerRef.current)) {
480
+ iosSyncTimerRef.current = setTimeout(() => {
481
+ resolveIOSSyncRequest(iosSelectedIndexRef.current);
482
+ }, IOS_CONFIRM_SYNC_TIMEOUT_MS);
483
+ return;
484
+ }
485
+
486
+ resolveIOSSyncRequest(iosSelectedIndexRef.current);
487
+ });
488
+ }, [clearIOSSettleTimer, resolveIOSSyncRequest, settleToNearest]);
489
+
490
+ React.useImperativeHandle(
491
+ ref,
492
+ () => ({ scrollToIndex, scrollToValue, settleToNearest, syncCurrentSelection }),
493
+ [scrollToIndex, scrollToValue, settleToNearest, syncCurrentSelection]
494
+ );
495
+
496
+ React.useEffect(() => {
497
+ selectedValueRef.current = resolvedValue;
498
+ displayIndexRef.current = displayIndex;
499
+ }, [displayIndex, resolvedValue]);
500
+
501
+ React.useEffect(() => {
502
+ if (isControlled || !options.length) return;
503
+ if (findOptionIndex(options, uncontrolledValue) >= 0) return;
504
+
505
+ const firstEnabledIndex = findNearestEnabledIndex(options, 0);
506
+ const nextValue = firstEnabledIndex >= 0 ? options[firstEnabledIndex]?.value : options[0]?.value;
507
+ setUncontrolledValue(nextValue ?? null);
508
+ }, [isControlled, options, uncontrolledValue]);
509
+
510
+ React.useEffect(() => {
511
+ if (!options.length) {
512
+ clearIOSSettleTimer();
513
+ iosSelectedIndexRef.current = 0;
514
+ setIosSelectedIndex(0);
515
+ cancelAnimation(offsetY);
516
+ offsetY.value = 0;
517
+ return;
518
+ }
519
+
520
+ clearIOSSettleTimer();
521
+ syncIOSSelectedIndex(displayIndex);
522
+ scrollToIndex(displayIndex, false);
523
+ }, [clearIOSSettleTimer, displayIndex, offsetY, options.length, scrollToIndex, syncIOSSelectedIndex]);
524
+
525
+ React.useEffect(
526
+ () => () => {
527
+ clearIOSSettleTimer();
528
+ resolveIOSSyncRequest(iosSelectedIndexRef.current);
529
+ },
530
+ [clearIOSSettleTimer, resolveIOSSyncRequest]
531
+ );
532
+
533
+ const handleIOSChange = React.useCallback(
534
+ (event: { nativeEvent: ZKitWheelPickerChangeEvent }) => {
535
+ const rawIndex = syncIOSSelectedIndex(event.nativeEvent.newIndex);
536
+ const nextIndex = resolveSelectableIndex(rawIndex, rawIndex - displayIndexRef.current);
537
+ resolveIOSSyncRequest(nextIndex);
538
+ scheduleIOSSettle(nextIndex);
539
+ },
540
+ [resolveIOSSyncRequest, resolveSelectableIndex, scheduleIOSSettle, syncIOSSelectedIndex]
541
+ );
542
+
543
+ const startInteraction = React.useCallback(() => {
544
+ cancelAnimation(offsetY);
545
+ isUserInteracting.value = true;
546
+ dragStartOffset.value = offsetY.value;
547
+ }, [dragStartOffset, isUserInteracting, offsetY]);
548
+
549
+ const updateInteraction = React.useCallback(
550
+ (translationY: number) => {
551
+ offsetY.value = clampNumber(dragStartOffset.value - translationY, 0, maxOffset);
552
+ },
553
+ [dragStartOffset, maxOffset, offsetY]
554
+ );
555
+
556
+ const finishInteraction = React.useCallback(
557
+ (velocityY: number) => {
558
+ const currentOffset = clampNumber(offsetY.value, 0, maxOffset);
559
+ const rawIndex = getTargetIndexFromRelease(currentOffset, velocityY, maxIndex);
560
+ const currentIndex = Math.round(currentOffset / WHEEL_ITEM_HEIGHT);
561
+ const nextIndex = resolveSelectableIndex(rawIndex, rawIndex - currentIndex);
562
+ const nextOffset = indexToOffset(nextIndex);
563
+ offsetY.value = withTiming(
564
+ nextOffset,
565
+ {
566
+ duration: getSnapDuration(currentOffset, nextOffset),
567
+ easing: SNAP_EASING,
568
+ },
569
+ (finished) => {
570
+ if (!finished) return;
571
+ isUserInteracting.value = false;
572
+ scheduleOnRN(emitSelectedIndex, nextIndex, 'user');
573
+ }
574
+ );
575
+ },
576
+ [emitSelectedIndex, isUserInteracting, maxIndex, maxOffset, offsetY, resolveSelectableIndex]
577
+ );
578
+
579
+ const touchStateRef = React.useRef({
580
+ startPageY: 0,
581
+ samples: [] as Array<{ pageY: number; timestamp: number }>,
582
+ });
583
+
584
+ const beginTouch = React.useCallback(
585
+ (pageY: number, timestamp?: number) => {
586
+ const ts = typeof timestamp === 'number' ? timestamp : Date.now();
587
+ touchStateRef.current = {
588
+ startPageY: pageY,
589
+ samples: [{ pageY, timestamp: ts }],
590
+ };
591
+ startInteraction();
592
+ },
593
+ [startInteraction]
594
+ );
595
+
596
+ const recordTouchSample = React.useCallback((pageY: number, timestamp?: number) => {
597
+ const ts = typeof timestamp === 'number' ? timestamp : Date.now();
598
+ const { samples } = touchStateRef.current;
599
+ samples.push({ pageY, timestamp: ts });
600
+
601
+ while (samples.length > 6) {
602
+ samples.shift();
603
+ }
604
+
605
+ const minTs = ts - ANDROID_VELOCITY_WINDOW_MS;
606
+ while (samples.length > 2 && samples[0]?.timestamp < minTs) {
607
+ samples.shift();
608
+ }
609
+ }, []);
610
+
611
+ const moveTouch = React.useCallback(
612
+ (pageY: number, timestamp?: number) => {
613
+ updateInteraction(pageY - touchStateRef.current.startPageY);
614
+ recordTouchSample(pageY, timestamp);
615
+ },
616
+ [recordTouchSample, updateInteraction]
617
+ );
618
+
619
+ const endTouch = React.useCallback(
620
+ (pageY: number, timestamp?: number) => {
621
+ recordTouchSample(pageY, timestamp);
622
+ const { samples } = touchStateRef.current;
623
+ const firstSample = samples[0];
624
+ const lastSample = samples[samples.length - 1];
625
+ const deltaY = (lastSample?.pageY ?? pageY) - (firstSample?.pageY ?? pageY);
626
+ const deltaT = Math.max(1, (lastSample?.timestamp ?? Date.now()) - (firstSample?.timestamp ?? Date.now()));
627
+ finishInteraction((deltaY / deltaT) * 1000);
628
+ },
629
+ [finishInteraction, recordTouchSample]
630
+ );
631
+
632
+ const handleAccessibilityAction = React.useCallback(
633
+ (event: AccessibilityActionEvent) => {
634
+ if (disabled) return;
635
+
636
+ const actionName = event.nativeEvent.actionName;
637
+ const direction = actionName === 'increment' ? 1 : actionName === 'decrement' ? -1 : 0;
638
+ if (direction === 0) return;
639
+
640
+ const nextIndex = findNearestEnabledIndex(options, displayIndexRef.current + direction, direction);
641
+ if (nextIndex < 0) return;
642
+
643
+ scrollToIndex(nextIndex, true);
644
+ emitSelectedIndex(nextIndex, 'accessibility');
645
+ },
646
+ [disabled, emitSelectedIndex, options, scrollToIndex]
647
+ );
648
+
649
+ const contentStyle = useAnimatedStyle(() => ({
650
+ transform: [{ translateY: CENTER_OFFSET - offsetY.value }],
651
+ }));
652
+
653
+ const items = React.useMemo(
654
+ () =>
655
+ options.map((item, index) => (
656
+ <View key={`${String(optionKey(item, index))}-${index}`} style={styles.itemContainer}>
657
+ <Animated.Text
658
+ accessibilityLabel={item.accessibilityLabel ?? item.label}
659
+ maxFontSizeMultiplier={getMaxFontSizeMultiplier()}
660
+ numberOfLines={numberOfLines}
661
+ style={[
662
+ styles.itemText,
663
+ { color: itemTextColor },
664
+ itemTextStyle,
665
+ index === displayIndex && [styles.itemTextSelected, { color: selectedTextColor }, selectedItemTextStyle],
666
+ item.disabled && [styles.itemTextDisabled, { color: disabledTextColor }, disabledItemTextStyle],
667
+ ]}
668
+ testID={item.testID}
669
+ >
670
+ {item.label}
671
+ </Animated.Text>
672
+ </View>
673
+ )),
674
+ [
675
+ disabledItemTextStyle,
676
+ disabledTextColor,
677
+ displayIndex,
678
+ itemTextColor,
679
+ itemTextStyle,
680
+ numberOfLines,
681
+ options,
682
+ selectedItemTextStyle,
683
+ selectedTextColor,
684
+ ]
685
+ );
686
+
687
+ const nativeItems = React.useMemo(
688
+ () =>
689
+ options.map((item, index) => ({
690
+ label: item.label,
691
+ value: item.value,
692
+ textColor: item.disabled ? disabledTextColor : undefined,
693
+ testID: item.testID ?? `wheel-item-${String(item.value)}-${index}`,
694
+ })),
695
+ [disabledTextColor, options]
696
+ );
697
+
698
+ const selectedOption = options[displayIndex];
699
+ const canInteract = !disabled && options.length > 1 && findNearestEnabledIndex(options, displayIndex) >= 0;
700
+ const columnStyle = React.useMemo(
701
+ () => [styles.column, width != null && { width }, resolveWebInteractionStyle(disabled), style],
702
+ [disabled, style, width]
703
+ );
704
+ const resolvedAccessibilityState = React.useMemo(
705
+ () => ({
706
+ ...accessibilityState,
707
+ disabled,
708
+ }),
709
+ [accessibilityState, disabled]
710
+ );
711
+
712
+ if (Platform.OS === 'ios') {
713
+ return (
714
+ <View
715
+ {...viewProps}
716
+ accessibilityHint={accessibilityHint}
717
+ accessibilityLabel={accessibilityLabel}
718
+ accessibilityState={resolvedAccessibilityState}
719
+ accessibilityValue={{ text: selectedOption?.label ?? '' }}
720
+ style={columnStyle}
721
+ pointerEvents={canInteract ? 'auto' : 'none'}
722
+ testID={testID}
723
+ >
724
+ <ZKitWheelPicker
725
+ ref={iosPickerRef as React.Ref<any>}
726
+ items={nativeItems}
727
+ selectedIndex={iosSelectedIndex}
728
+ onChange={handleIOSChange}
729
+ numberOfLines={numberOfLines}
730
+ rowHeight={IOS_NATIVE_PICKER_ROW_HEIGHT}
731
+ style={styles.iosPicker}
732
+ fontFamily={nativeFontFamily}
733
+ fontSize={nativeFontSize}
734
+ fontStyle={nativeFontStyle}
735
+ fontWeight={nativeFontWeight}
736
+ color={selectedTextColor}
737
+ />
738
+ </View>
739
+ );
740
+ }
741
+
742
+ return (
743
+ <View
744
+ {...viewProps}
745
+ accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
746
+ accessibilityHint={accessibilityHint}
747
+ accessibilityLabel={accessibilityLabel}
748
+ accessibilityRole="adjustable"
749
+ accessibilityState={resolvedAccessibilityState}
750
+ accessibilityValue={{ text: selectedOption?.label ?? '' }}
751
+ collapsable={false}
752
+ onAccessibilityAction={handleAccessibilityAction}
753
+ style={columnStyle}
754
+ testID={testID}
755
+ >
756
+ <Animated.View
757
+ style={[styles.content, contentStyle]}
758
+ pointerEvents="none"
759
+ renderToHardwareTextureAndroid={Platform.OS === 'android'}
760
+ shouldRasterizeIOS={false}
761
+ >
762
+ {items}
763
+ </Animated.View>
764
+ <View
765
+ style={styles.touchSurface}
766
+ collapsable={false}
767
+ pointerEvents="box-only"
768
+ onStartShouldSetResponder={() => canInteract}
769
+ onStartShouldSetResponderCapture={() => canInteract}
770
+ onMoveShouldSetResponder={() => canInteract}
771
+ onMoveShouldSetResponderCapture={() => canInteract}
772
+ onResponderTerminationRequest={() => false}
773
+ onResponderGrant={(event) => {
774
+ beginTouch(event.nativeEvent.pageY, event.nativeEvent.timestamp);
775
+ }}
776
+ onResponderMove={(event) => {
777
+ moveTouch(event.nativeEvent.pageY, event.nativeEvent.timestamp);
778
+ }}
779
+ onResponderRelease={(event) => {
780
+ endTouch(event.nativeEvent.pageY, event.nativeEvent.timestamp);
781
+ }}
782
+ onResponderTerminate={(event) => {
783
+ endTouch(event.nativeEvent.pageY, event.nativeEvent.timestamp);
784
+ }}
785
+ />
786
+ </View>
787
+ );
788
+ });
789
+
790
+ export const WheelColumn = React.memo(WheelColumnBase);
791
+
792
+ const styles = StyleSheet.create({
793
+ column: {
794
+ height: WHEEL_VIEWPORT_HEIGHT,
795
+ overflow: 'hidden',
796
+ },
797
+ iosPicker: {
798
+ width: '100%',
799
+ height: IOS_NATIVE_PICKER_HEIGHT,
800
+ },
801
+ content: {
802
+ width: '100%',
803
+ },
804
+ touchSurface: {
805
+ ...StyleSheet.absoluteFillObject,
806
+ backgroundColor: 'transparent',
807
+ zIndex: 5,
808
+ },
809
+ itemContainer: {
810
+ height: WHEEL_ITEM_HEIGHT,
811
+ justifyContent: 'center',
812
+ alignItems: 'center',
813
+ paddingHorizontal: wp(8),
814
+ },
815
+ itemText: {
816
+ fontSize: sp(18),
817
+ textAlign: 'center',
818
+ fontWeight: '500',
819
+ },
820
+ itemTextSelected: {
821
+ opacity: 1,
822
+ },
823
+ itemTextDisabled: {
824
+ opacity: 0.62,
825
+ },
826
+ });