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,816 @@
1
+ import { Feather } from '@expo/vector-icons';
2
+ import * as React from 'react';
3
+ import {
4
+ ActivityIndicator,
5
+ BackHandler,
6
+ Image,
7
+ Platform,
8
+ PanResponder,
9
+ Pressable,
10
+ StyleProp,
11
+ StyleSheet,
12
+ View,
13
+ ViewStyle,
14
+ type LayoutChangeEvent,
15
+ } from 'react-native';
16
+ import Animated, {
17
+ Easing,
18
+ cancelAnimation,
19
+ useAnimatedStyle,
20
+ useSharedValue,
21
+ withTiming,
22
+ } from 'react-native-reanimated';
23
+ import { scheduleOnRN } from 'react-native-worklets';
24
+ import { wp } from 'zkit-tools';
25
+ import { useI18n } from '../../i18n/useI18n';
26
+ import { useTheme } from '../../theme/useTheme';
27
+ import { Text } from '../Text';
28
+
29
+ const DEFAULT_CHALLENGE_WIDTH = 320;
30
+ const DEFAULT_CHALLENGE_HEIGHT = 220;
31
+ const DEFAULT_BLOCK_SIZE = 50;
32
+ const DEFAULT_SUCCESS_DURATION = 500;
33
+ const DEFAULT_ERROR_DURATION = 700;
34
+ const DEFAULT_CARD_MAX_WIDTH = wp(340);
35
+ const CARD_HORIZONTAL_PADDING = wp(24);
36
+ const CANVAS_BORDER_WIDTH = wp(1);
37
+ const CAPTCHA_LAYER_Z_INDEX = 4000;
38
+ const SLIDER_THUMB_SIZE = wp(44);
39
+ const CAPTCHA_ENTER_DURATION = 280;
40
+ const CAPTCHA_EXIT_DURATION = 220;
41
+ const CAPTCHA_CARD_OFFSET_Y = wp(18);
42
+ const CAPTCHA_ENTER_EASING = Easing.bezier(0.22, 1, 0.36, 1);
43
+ const CAPTCHA_EXIT_EASING = Easing.out(Easing.cubic);
44
+
45
+ export type SliderCaptchaStatus = 'idle' | 'loading' | 'verifying' | 'success' | 'error';
46
+ export type SliderCaptchaLoadReason = 'open' | 'refresh' | 'retry';
47
+ export type SliderCaptchaErrorStage = 'load' | 'verify';
48
+
49
+ export type SliderCaptchaTexts = {
50
+ title: string;
51
+ verifyFailed: string;
52
+ verifySuccess: string;
53
+ };
54
+
55
+ export type SliderCaptchaChallenge = {
56
+ backgroundImage: string;
57
+ blockImage: string;
58
+ blockY: number;
59
+ originalWidth: number;
60
+ originalHeight: number;
61
+ blockWidth: number;
62
+ blockHeight?: number;
63
+ };
64
+
65
+ export type SliderCaptchaVerifyPayload<
66
+ TChallenge extends SliderCaptchaChallenge = SliderCaptchaChallenge,
67
+ > = {
68
+ challenge: TChallenge;
69
+ offsetX: number;
70
+ offsetY: number;
71
+ displayOffsetX: number;
72
+ progress: number;
73
+ scale: number;
74
+ };
75
+
76
+ export type SliderCaptchaVerifyResult =
77
+ | boolean
78
+ | void
79
+ | {
80
+ success: boolean;
81
+ message?: string;
82
+ };
83
+
84
+ export type SliderCaptchaErrorInfo<
85
+ TChallenge extends SliderCaptchaChallenge = SliderCaptchaChallenge,
86
+ > = {
87
+ stage: SliderCaptchaErrorStage;
88
+ reason?: SliderCaptchaLoadReason;
89
+ challenge?: TChallenge;
90
+ };
91
+
92
+ export type SliderCaptchaProps<
93
+ TChallenge extends SliderCaptchaChallenge = SliderCaptchaChallenge,
94
+ > = {
95
+ visible: boolean;
96
+ onClose: () => void;
97
+ initialChallenge?: TChallenge | null;
98
+ loadChallenge:
99
+ | ((context: { reason: SliderCaptchaLoadReason }) => Promise<TChallenge>)
100
+ | ((context: { reason: SliderCaptchaLoadReason }) => TChallenge);
101
+ verifyChallenge:
102
+ | ((
103
+ payload: SliderCaptchaVerifyPayload<TChallenge>
104
+ ) => Promise<SliderCaptchaVerifyResult>)
105
+ | ((payload: SliderCaptchaVerifyPayload<TChallenge>) => SliderCaptchaVerifyResult);
106
+ onVerified?: (payload: SliderCaptchaVerifyPayload<TChallenge>) => void;
107
+ onError?: (error: unknown, info: SliderCaptchaErrorInfo<TChallenge>) => void;
108
+ texts?: Partial<SliderCaptchaTexts>;
109
+ dismissOnBackdropPress?: boolean;
110
+ reloadOnOpen?: boolean;
111
+ successFeedbackDuration?: number;
112
+ errorFeedbackDuration?: number;
113
+ maxCardWidth?: number;
114
+ cardStyle?: StyleProp<ViewStyle>;
115
+ };
116
+
117
+ function clamp(value: number, min: number, max: number) {
118
+ return Math.min(Math.max(value, min), max);
119
+ }
120
+
121
+ function normalizeImageUri(value: string) {
122
+ if (!value) return '';
123
+ if (/^(data:|https?:\/\/|file:\/\/|content:\/\/|asset:|ph:)/.test(value)) {
124
+ return value;
125
+ }
126
+ return `data:image/png;base64,${value}`;
127
+ }
128
+
129
+ function parseVerifyResult(
130
+ result: SliderCaptchaVerifyResult
131
+ ): {
132
+ success: boolean;
133
+ message?: string;
134
+ } {
135
+ if (typeof result === 'object' && result !== null) {
136
+ return {
137
+ success: Boolean(result.success),
138
+ message: result.message,
139
+ };
140
+ }
141
+ if (typeof result === 'boolean') {
142
+ return { success: result };
143
+ }
144
+ return { success: true };
145
+ }
146
+
147
+ function getSafePositiveNumber(value: number | undefined, fallback: number) {
148
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
149
+ return fallback;
150
+ }
151
+ return value;
152
+ }
153
+
154
+ const styles = StyleSheet.create({
155
+ modalRoot: {
156
+ ...StyleSheet.absoluteFillObject,
157
+ zIndex: CAPTCHA_LAYER_Z_INDEX,
158
+ elevation: Platform.OS === 'android' ? CAPTCHA_LAYER_Z_INDEX : 0,
159
+ },
160
+ overlayBackdrop: {
161
+ ...StyleSheet.absoluteFillObject,
162
+ backgroundColor: 'rgba(0,0,0,0.55)',
163
+ },
164
+ overlayPressable: {
165
+ ...StyleSheet.absoluteFillObject,
166
+ justifyContent: 'center',
167
+ alignItems: 'center',
168
+ paddingHorizontal: wp(20),
169
+ },
170
+ cardContainer: {
171
+ width: '100%',
172
+ alignItems: 'center',
173
+ },
174
+ card: {
175
+ width: '100%',
176
+ borderRadius: wp(16),
177
+ paddingHorizontal: CARD_HORIZONTAL_PADDING,
178
+ paddingTop: wp(18),
179
+ paddingBottom: wp(20),
180
+ shadowOffset: { width: 0, height: wp(10) },
181
+ shadowOpacity: 0.1,
182
+ shadowRadius: wp(20),
183
+ elevation: 10,
184
+ },
185
+ titleRow: {
186
+ flexDirection: 'row',
187
+ alignItems: 'center',
188
+ justifyContent: 'space-between',
189
+ marginBottom: wp(14),
190
+ },
191
+ title: {
192
+ fontSize: wp(16),
193
+ fontWeight: '500',
194
+ },
195
+ headerActions: {
196
+ flexDirection: 'row',
197
+ alignItems: 'center',
198
+ },
199
+ headerActionButton: {
200
+ width: wp(28),
201
+ height: wp(28),
202
+ borderRadius: wp(14),
203
+ alignItems: 'center',
204
+ justifyContent: 'center',
205
+ shadowOffset: { width: 0, height: wp(2) },
206
+ shadowOpacity: 0.08,
207
+ shadowRadius: wp(4),
208
+ elevation: 3,
209
+ },
210
+ headerActionSpacing: {
211
+ marginLeft: wp(8),
212
+ },
213
+ canvasWrapper: {
214
+ width: '100%',
215
+ overflow: 'hidden',
216
+ alignSelf: 'center',
217
+ position: 'relative',
218
+ borderWidth: CANVAS_BORDER_WIDTH,
219
+ borderRadius: wp(16),
220
+ },
221
+ canvasImage: {
222
+ width: '100%',
223
+ height: '100%',
224
+ },
225
+ block: {
226
+ position: 'absolute',
227
+ left: 0,
228
+ },
229
+ feedbackOverlay: {
230
+ position: 'absolute',
231
+ left: 0,
232
+ right: 0,
233
+ bottom: 0,
234
+ height: wp(36),
235
+ alignItems: 'center',
236
+ justifyContent: 'center',
237
+ zIndex: 20,
238
+ },
239
+ feedbackError: {
240
+ backgroundColor: 'rgba(239, 68, 68, 0.9)',
241
+ },
242
+ feedbackSuccess: {
243
+ backgroundColor: 'rgba(34, 197, 94, 0.9)',
244
+ },
245
+ feedbackText: {
246
+ color: '#FFFFFF',
247
+ fontSize: wp(14),
248
+ fontWeight: '600',
249
+ letterSpacing: 0.5,
250
+ },
251
+ placeholder: {
252
+ alignItems: 'center',
253
+ justifyContent: 'center',
254
+ },
255
+ sliderTrack: {
256
+ marginTop: wp(16),
257
+ marginBottom: wp(4),
258
+ height: SLIDER_THUMB_SIZE,
259
+ borderRadius: SLIDER_THUMB_SIZE / 2,
260
+ overflow: 'hidden',
261
+ justifyContent: 'center',
262
+ },
263
+ sliderThumb: {
264
+ position: 'absolute',
265
+ left: 0,
266
+ width: SLIDER_THUMB_SIZE,
267
+ height: SLIDER_THUMB_SIZE,
268
+ borderRadius: SLIDER_THUMB_SIZE / 2,
269
+ alignItems: 'center',
270
+ justifyContent: 'center',
271
+ shadowOffset: { width: 0, height: wp(2) },
272
+ shadowOpacity: 0.15,
273
+ shadowRadius: wp(4),
274
+ elevation: 3,
275
+ borderWidth: wp(0.5),
276
+ },
277
+ });
278
+
279
+ export function SliderCaptcha<TChallenge extends SliderCaptchaChallenge = SliderCaptchaChallenge>({
280
+ visible,
281
+ onClose,
282
+ initialChallenge = null,
283
+ loadChallenge,
284
+ verifyChallenge,
285
+ onVerified,
286
+ onError,
287
+ texts,
288
+ dismissOnBackdropPress = true,
289
+ reloadOnOpen = true,
290
+ successFeedbackDuration = DEFAULT_SUCCESS_DURATION,
291
+ errorFeedbackDuration = DEFAULT_ERROR_DURATION,
292
+ maxCardWidth = DEFAULT_CARD_MAX_WIDTH,
293
+ cardStyle,
294
+ }: SliderCaptchaProps<TChallenge>) {
295
+ const { t } = useI18n();
296
+ const theme = useTheme();
297
+ const [mounted, setMounted] = React.useState(visible);
298
+ const [challenge, setChallenge] = React.useState<TChallenge | null>(null);
299
+ const [status, setStatus] = React.useState<SliderCaptchaStatus>('idle');
300
+ const [feedbackMessage, setFeedbackMessage] = React.useState<string | undefined>();
301
+ const [canvasLayoutWidth, setCanvasLayoutWidth] = React.useState(0);
302
+
303
+ const presentationProgress = useSharedValue(0);
304
+ const thumbX = useSharedValue(0);
305
+ const thumbXRef = React.useRef(0);
306
+ const thumbStartRef = React.useRef(0);
307
+ const visibleRef = React.useRef(visible);
308
+ const visibilityAnimationIdRef = React.useRef(0);
309
+ const loadRequestIdRef = React.useRef(0);
310
+ const verifyRequestIdRef = React.useRef(0);
311
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
312
+ const animationFrameRefs = React.useRef<number[]>([]);
313
+
314
+ const resolvedTexts = React.useMemo<SliderCaptchaTexts>(
315
+ () => ({
316
+ title: texts?.title ?? t('sliderCaptcha.title'),
317
+ verifyFailed: texts?.verifyFailed ?? t('sliderCaptcha.verifyFailed'),
318
+ verifySuccess: texts?.verifySuccess ?? t('sliderCaptcha.verifySuccess'),
319
+ }),
320
+ [t, texts?.title, texts?.verifyFailed, texts?.verifySuccess]
321
+ );
322
+
323
+ const clearPendingTimeout = React.useCallback(() => {
324
+ if (timeoutRef.current) {
325
+ clearTimeout(timeoutRef.current);
326
+ timeoutRef.current = null;
327
+ }
328
+ }, []);
329
+
330
+ const clearPendingAnimationFrames = React.useCallback(() => {
331
+ animationFrameRefs.current.forEach((frameId) => cancelAnimationFrame(frameId));
332
+ animationFrameRefs.current = [];
333
+ }, []);
334
+
335
+ const resetThumbPosition = React.useCallback(() => {
336
+ thumbX.value = 0;
337
+ thumbXRef.current = 0;
338
+ thumbStartRef.current = 0;
339
+ }, [thumbX]);
340
+
341
+ const resetCaptchaState = React.useCallback(() => {
342
+ setStatus('idle');
343
+ setFeedbackMessage(undefined);
344
+ setChallenge(null);
345
+ resetThumbPosition();
346
+ }, [resetThumbPosition]);
347
+
348
+ const originalWidth = getSafePositiveNumber(challenge?.originalWidth, DEFAULT_CHALLENGE_WIDTH);
349
+ const originalHeight = getSafePositiveNumber(challenge?.originalHeight, DEFAULT_CHALLENGE_HEIGHT);
350
+ const blockWidth = getSafePositiveNumber(challenge?.blockWidth, DEFAULT_BLOCK_SIZE);
351
+ const blockHeight = getSafePositiveNumber(challenge?.blockHeight, blockWidth);
352
+ const canvasOuterWidth =
353
+ canvasLayoutWidth > 0
354
+ ? canvasLayoutWidth
355
+ : Math.max(maxCardWidth - CARD_HORIZONTAL_PADDING * 2, 0);
356
+ // Use the drawable content box for coordinate mapping; including the border
357
+ // here introduces the exact sub-pixel Y drift seen when the puzzle piece joins.
358
+ const canvasContentWidth = Math.max(canvasOuterWidth - CANVAS_BORDER_WIDTH * 2, 0);
359
+ const scale = canvasContentWidth > 0 ? canvasContentWidth / originalWidth : 0;
360
+ const displayHeight =
361
+ Math.max((canvasContentWidth * originalHeight) / originalWidth, 0) + CANVAS_BORDER_WIDTH * 2;
362
+ const blockDisplayWidth = blockWidth * scale;
363
+ const blockDisplayHeight = blockHeight * scale;
364
+ const blockTop = Math.max(0, (challenge?.blockY ?? 0) * scale);
365
+ const maxBlockOffset = Math.max(canvasContentWidth - blockDisplayWidth, 0);
366
+ const maxThumbOffset = Math.max(canvasOuterWidth - SLIDER_THUMB_SIZE, 0);
367
+ const blockTranslateRatio = maxThumbOffset > 0 ? maxBlockOffset / maxThumbOffset : 0;
368
+ const isBusy = status === 'loading' || status === 'verifying';
369
+ const canSlide = visible && Boolean(challenge) && !isBusy;
370
+
371
+ const backgroundImageUri = normalizeImageUri(challenge?.backgroundImage ?? '');
372
+ const blockImageUri = normalizeImageUri(challenge?.blockImage ?? '');
373
+
374
+ const requestChallenge = React.useCallback(
375
+ async (reason: SliderCaptchaLoadReason) => {
376
+ if (!visibleRef.current) return;
377
+
378
+ clearPendingTimeout();
379
+ if (reason === 'open') {
380
+ setChallenge(null);
381
+ }
382
+ setFeedbackMessage(undefined);
383
+ setStatus('loading');
384
+
385
+ const requestId = loadRequestIdRef.current + 1;
386
+ loadRequestIdRef.current = requestId;
387
+
388
+ try {
389
+ const nextChallenge = await loadChallenge({ reason });
390
+ if (!visibleRef.current || loadRequestIdRef.current !== requestId) return;
391
+
392
+ setChallenge(nextChallenge);
393
+ resetThumbPosition();
394
+ setStatus('idle');
395
+ } catch (error) {
396
+ if (!visibleRef.current || loadRequestIdRef.current !== requestId) return;
397
+
398
+ setStatus('idle');
399
+ onError?.(error, { stage: 'load', reason });
400
+ }
401
+ },
402
+ [clearPendingTimeout, loadChallenge, onError, resetThumbPosition]
403
+ );
404
+
405
+ const verifyCurrentChallenge = React.useCallback(async () => {
406
+ if (!challenge || isBusy) return;
407
+
408
+ clearPendingTimeout();
409
+ setFeedbackMessage(undefined);
410
+ setStatus('verifying');
411
+
412
+ const progress = maxThumbOffset > 0 ? thumbXRef.current / maxThumbOffset : 0;
413
+ const displayOffsetX = progress * maxBlockOffset;
414
+ const offsetX = scale > 0 ? Math.round(displayOffsetX / scale) : 0;
415
+ const payload: SliderCaptchaVerifyPayload<TChallenge> = {
416
+ challenge,
417
+ offsetX,
418
+ offsetY: challenge.blockY,
419
+ displayOffsetX,
420
+ progress,
421
+ scale,
422
+ };
423
+
424
+ const requestId = verifyRequestIdRef.current + 1;
425
+ verifyRequestIdRef.current = requestId;
426
+
427
+ try {
428
+ const rawResult = await verifyChallenge(payload);
429
+ if (!visibleRef.current || verifyRequestIdRef.current !== requestId) return;
430
+
431
+ const result = parseVerifyResult(rawResult);
432
+ if (result.success) {
433
+ setFeedbackMessage(result.message ?? resolvedTexts.verifySuccess);
434
+ setStatus('success');
435
+ timeoutRef.current = setTimeout(() => {
436
+ if (!visibleRef.current || verifyRequestIdRef.current !== requestId) return;
437
+ onVerified?.(payload);
438
+ }, successFeedbackDuration);
439
+ return;
440
+ }
441
+
442
+ setFeedbackMessage(result.message ?? resolvedTexts.verifyFailed);
443
+ setStatus('error');
444
+ timeoutRef.current = setTimeout(() => {
445
+ if (!visibleRef.current || verifyRequestIdRef.current !== requestId) return;
446
+ void requestChallenge('retry');
447
+ }, errorFeedbackDuration);
448
+ } catch (error) {
449
+ if (!visibleRef.current || verifyRequestIdRef.current !== requestId) return;
450
+
451
+ setFeedbackMessage(resolvedTexts.verifyFailed);
452
+ setStatus('error');
453
+ onError?.(error, { stage: 'verify', challenge });
454
+ timeoutRef.current = setTimeout(() => {
455
+ if (!visibleRef.current || verifyRequestIdRef.current !== requestId) return;
456
+ void requestChallenge('retry');
457
+ }, errorFeedbackDuration);
458
+ }
459
+ }, [
460
+ challenge,
461
+ clearPendingTimeout,
462
+ errorFeedbackDuration,
463
+ isBusy,
464
+ maxBlockOffset,
465
+ maxThumbOffset,
466
+ onError,
467
+ onVerified,
468
+ requestChallenge,
469
+ resolvedTexts.verifyFailed,
470
+ resolvedTexts.verifySuccess,
471
+ scale,
472
+ successFeedbackDuration,
473
+ verifyChallenge,
474
+ ]);
475
+
476
+ React.useEffect(() => {
477
+ visibleRef.current = visible;
478
+ }, [visible]);
479
+
480
+ const handleDismissAnimationFinished = React.useCallback(
481
+ (animationId: number) => {
482
+ if (animationId !== visibilityAnimationIdRef.current || visibleRef.current) return;
483
+ setMounted(false);
484
+ resetCaptchaState();
485
+ },
486
+ [resetCaptchaState]
487
+ );
488
+
489
+ React.useEffect(() => {
490
+ if (visible && !mounted) {
491
+ setMounted(true);
492
+ }
493
+ }, [mounted, visible]);
494
+
495
+ React.useEffect(() => {
496
+ if (!mounted) return;
497
+
498
+ clearPendingAnimationFrames();
499
+ cancelAnimation(presentationProgress);
500
+
501
+ const animationId = visibilityAnimationIdRef.current + 1;
502
+ visibilityAnimationIdRef.current = animationId;
503
+
504
+ if (visible) {
505
+ setStatus('idle');
506
+ setFeedbackMessage(undefined);
507
+ resetThumbPosition();
508
+
509
+ const frameId = requestAnimationFrame(() => {
510
+ const nestedFrameId = requestAnimationFrame(() => {
511
+ if (visibilityAnimationIdRef.current !== animationId || !visibleRef.current) return;
512
+ presentationProgress.value = withTiming(1, {
513
+ duration: CAPTCHA_ENTER_DURATION,
514
+ easing: CAPTCHA_ENTER_EASING,
515
+ });
516
+ });
517
+ animationFrameRefs.current.push(nestedFrameId);
518
+ });
519
+ animationFrameRefs.current.push(frameId);
520
+
521
+ if (initialChallenge) {
522
+ setChallenge(initialChallenge);
523
+ } else if (reloadOnOpen || challenge === null) {
524
+ void requestChallenge('open');
525
+ }
526
+
527
+ return () => {
528
+ clearPendingAnimationFrames();
529
+ };
530
+ }
531
+
532
+ loadRequestIdRef.current += 1;
533
+ verifyRequestIdRef.current += 1;
534
+ clearPendingTimeout();
535
+ setStatus('idle');
536
+ setFeedbackMessage(undefined);
537
+
538
+ presentationProgress.value = withTiming(
539
+ 0,
540
+ {
541
+ duration: CAPTCHA_EXIT_DURATION,
542
+ easing: CAPTCHA_EXIT_EASING,
543
+ },
544
+ (finished) => {
545
+ if (finished) {
546
+ scheduleOnRN(handleDismissAnimationFinished, animationId);
547
+ }
548
+ }
549
+ );
550
+ }, [
551
+ challenge,
552
+ clearPendingAnimationFrames,
553
+ clearPendingTimeout,
554
+ handleDismissAnimationFinished,
555
+ initialChallenge,
556
+ mounted,
557
+ presentationProgress,
558
+ reloadOnOpen,
559
+ requestChallenge,
560
+ resetThumbPosition,
561
+ visible,
562
+ ]);
563
+
564
+ React.useEffect(
565
+ () => () => {
566
+ clearPendingAnimationFrames();
567
+ clearPendingTimeout();
568
+ cancelAnimation(presentationProgress);
569
+ },
570
+ [clearPendingAnimationFrames, clearPendingTimeout, presentationProgress]
571
+ );
572
+
573
+ React.useEffect(() => {
574
+ if (!visible || Platform.OS !== 'android') return;
575
+
576
+ const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
577
+ onClose();
578
+ return true;
579
+ });
580
+
581
+ return () => subscription.remove();
582
+ }, [onClose, visible]);
583
+
584
+ const handleCanvasLayout = React.useCallback((event: LayoutChangeEvent) => {
585
+ const nextWidth = event.nativeEvent.layout.width;
586
+ if (!Number.isFinite(nextWidth) || nextWidth <= 0) return;
587
+
588
+ setCanvasLayoutWidth((currentWidth) => {
589
+ if (Math.abs(currentWidth - nextWidth) < 0.5) {
590
+ return currentWidth;
591
+ }
592
+ return nextWidth;
593
+ });
594
+ }, []);
595
+
596
+ const handleRefresh = React.useCallback(() => {
597
+ if (!visible || isBusy) return;
598
+ void requestChallenge('refresh');
599
+ }, [isBusy, requestChallenge, visible]);
600
+
601
+ const handleBackdropPress = React.useCallback(() => {
602
+ if (!visible || !dismissOnBackdropPress) return;
603
+ onClose();
604
+ }, [dismissOnBackdropPress, onClose, visible]);
605
+
606
+ const handleRequestClose = React.useCallback(() => {
607
+ if (!visible) return;
608
+ onClose();
609
+ }, [onClose, visible]);
610
+
611
+ const backdropAnimatedStyle = useAnimatedStyle(() => ({
612
+ opacity: presentationProgress.value,
613
+ }));
614
+
615
+ const cardAnimatedStyle = useAnimatedStyle(() => {
616
+ const progress = presentationProgress.value;
617
+
618
+ return {
619
+ opacity: progress,
620
+ transform: [
621
+ { translateY: (1 - progress) * CAPTCHA_CARD_OFFSET_Y },
622
+ { scale: 0.96 + progress * 0.04 },
623
+ ],
624
+ };
625
+ });
626
+
627
+ const thumbAnimatedStyle = useAnimatedStyle(() => ({
628
+ transform: [{ translateX: thumbX.value }],
629
+ }));
630
+
631
+ const blockAnimatedStyle = useAnimatedStyle(() => ({
632
+ transform: [{ translateX: thumbX.value * blockTranslateRatio }],
633
+ }));
634
+
635
+ const panResponder = React.useMemo(
636
+ () =>
637
+ PanResponder.create({
638
+ onStartShouldSetPanResponder: () => canSlide,
639
+ onMoveShouldSetPanResponder: () => canSlide,
640
+ onPanResponderGrant: () => {
641
+ thumbStartRef.current = thumbXRef.current;
642
+ },
643
+ onPanResponderMove: (_event, gestureState) => {
644
+ if (!canSlide) return;
645
+
646
+ const next = clamp(thumbStartRef.current + gestureState.dx, 0, maxThumbOffset);
647
+ thumbX.value = next;
648
+ thumbXRef.current = next;
649
+ },
650
+ onPanResponderRelease: () => {
651
+ void verifyCurrentChallenge();
652
+ },
653
+ }),
654
+ [canSlide, maxThumbOffset, thumbX, verifyCurrentChallenge]
655
+ );
656
+
657
+ if (!mounted) return null;
658
+
659
+ return (
660
+ <View pointerEvents="box-none" style={styles.modalRoot}>
661
+ <Animated.View style={[styles.overlayBackdrop, backdropAnimatedStyle]} />
662
+ <Pressable style={styles.overlayPressable} onPress={handleBackdropPress}>
663
+ {/* Fade a single composited layer to avoid dark ghosting on dismiss, especially on Android. */}
664
+ <Animated.View
665
+ style={[styles.cardContainer, cardAnimatedStyle]}
666
+ collapsable={false}
667
+ renderToHardwareTextureAndroid={Platform.OS === 'android'}
668
+ shouldRasterizeIOS={Platform.OS === 'ios'}
669
+ needsOffscreenAlphaCompositing={Platform.OS === 'android'}
670
+ >
671
+ <Pressable
672
+ style={[
673
+ styles.card,
674
+ {
675
+ maxWidth: maxCardWidth,
676
+ backgroundColor: theme.colors.surface,
677
+ shadowColor: theme.colors.onSurface,
678
+ },
679
+ cardStyle,
680
+ ]}
681
+ onPress={() => {}}
682
+ collapsable={false}
683
+ renderToHardwareTextureAndroid={Platform.OS === 'android'}
684
+ shouldRasterizeIOS={Platform.OS === 'ios'}
685
+ needsOffscreenAlphaCompositing={Platform.OS === 'android'}
686
+ >
687
+ <View style={styles.titleRow}>
688
+ <Text style={[styles.title, { color: theme.colors.onSurface }]}>
689
+ {resolvedTexts.title}
690
+ </Text>
691
+ <View style={styles.headerActions}>
692
+ <Pressable
693
+ style={[
694
+ styles.headerActionButton,
695
+ {
696
+ backgroundColor: theme.colors.surface,
697
+ borderColor: theme.colors.border,
698
+ borderWidth: wp(0.5),
699
+ shadowColor: theme.colors.onSurface,
700
+ },
701
+ ]}
702
+ onPress={handleRefresh}
703
+ disabled={!visible || isBusy}
704
+ hitSlop={10}
705
+ >
706
+ {isBusy ? (
707
+ <ActivityIndicator size="small" color={theme.colors.muted} />
708
+ ) : (
709
+ <Feather name="refresh-cw" size={wp(16)} color={theme.colors.muted} />
710
+ )}
711
+ </Pressable>
712
+ <Pressable
713
+ style={[
714
+ styles.headerActionButton,
715
+ styles.headerActionSpacing,
716
+ {
717
+ backgroundColor: theme.colors.secondary,
718
+ shadowColor: theme.colors.onSurface,
719
+ },
720
+ ]}
721
+ onPress={handleRequestClose}
722
+ disabled={!visible}
723
+ hitSlop={10}
724
+ >
725
+ <Feather name="x" size={wp(18)} color={theme.colors.muted} />
726
+ </Pressable>
727
+ </View>
728
+ </View>
729
+
730
+ <View
731
+ style={[
732
+ styles.canvasWrapper,
733
+ {
734
+ height: displayHeight,
735
+ backgroundColor: theme.colors.secondary,
736
+ borderColor: theme.colors.border,
737
+ },
738
+ ]}
739
+ onLayout={handleCanvasLayout}
740
+ >
741
+ {backgroundImageUri ? (
742
+ <Image
743
+ style={styles.canvasImage}
744
+ resizeMode="cover"
745
+ source={{ uri: backgroundImageUri }}
746
+ />
747
+ ) : (
748
+ <View style={[styles.canvasImage, styles.placeholder]}>
749
+ <ActivityIndicator size="small" color={theme.colors.muted} />
750
+ </View>
751
+ )}
752
+
753
+ {blockImageUri ? (
754
+ <Animated.View
755
+ pointerEvents="none"
756
+ style={[
757
+ styles.block,
758
+ {
759
+ top: blockTop,
760
+ width: blockDisplayWidth,
761
+ height: blockDisplayHeight,
762
+ },
763
+ blockAnimatedStyle,
764
+ ]}
765
+ >
766
+ <Image
767
+ style={styles.canvasImage}
768
+ resizeMode="cover"
769
+ source={{ uri: blockImageUri }}
770
+ />
771
+ </Animated.View>
772
+ ) : null}
773
+
774
+ {(status === 'error' || status === 'success') && feedbackMessage ? (
775
+ <View
776
+ style={[
777
+ styles.feedbackOverlay,
778
+ status === 'error' ? styles.feedbackError : styles.feedbackSuccess,
779
+ ]}
780
+ >
781
+ <Text style={styles.feedbackText}>{feedbackMessage}</Text>
782
+ </View>
783
+ ) : null}
784
+ </View>
785
+
786
+ <View
787
+ style={[
788
+ styles.sliderTrack,
789
+ { backgroundColor: theme.colors.secondary, borderColor: theme.colors.border },
790
+ ]}
791
+ {...panResponder.panHandlers}
792
+ >
793
+ <Animated.View
794
+ style={[
795
+ styles.sliderThumb,
796
+ thumbAnimatedStyle,
797
+ {
798
+ backgroundColor: theme.colors.primary,
799
+ borderColor: 'rgba(0,0,0,0.04)',
800
+ shadowColor: theme.colors.onSurface,
801
+ },
802
+ ]}
803
+ >
804
+ {isBusy ? (
805
+ <ActivityIndicator size="small" color={theme.colors.onPrimary} />
806
+ ) : (
807
+ <Feather name="arrow-right" size={wp(20)} color={theme.colors.onPrimary} />
808
+ )}
809
+ </Animated.View>
810
+ </View>
811
+ </Pressable>
812
+ </Animated.View>
813
+ </Pressable>
814
+ </View>
815
+ );
816
+ }