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,1092 @@
1
+ import * as React from 'react';
2
+ import type {
3
+ GestureResponderEvent,
4
+ Insets,
5
+ PressableStateCallbackType,
6
+ StyleProp,
7
+ TextStyle,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+ import {
11
+ I18nManager,
12
+ Platform,
13
+ Pressable,
14
+ processColor,
15
+ StyleSheet,
16
+ useColorScheme,
17
+ useWindowDimensions,
18
+ View,
19
+ } from 'react-native';
20
+ import Animated, {
21
+ Easing,
22
+ interpolate,
23
+ interpolateColor,
24
+ ReduceMotion,
25
+ useAnimatedStyle,
26
+ useSharedValue,
27
+ withTiming,
28
+ } from 'react-native-reanimated';
29
+ import { wp } from 'zkit-tools';
30
+ import type { Theme } from '../../theme/types';
31
+ import { useTheme } from '../../theme/useTheme';
32
+ import { LoadingSpinner } from '../LoadingSpinner';
33
+ import { Text } from '../Text';
34
+
35
+ export type SwitchSize = 'sm' | 'md' | 'lg';
36
+ export type SwitchTone = 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info';
37
+ export type SwitchLabelPlacement = 'start' | 'end';
38
+
39
+ export type SwitchStateText = {
40
+ checked?: string;
41
+ unchecked?: string;
42
+ };
43
+
44
+ export type SwitchLayout = {
45
+ width?: number;
46
+ height?: number;
47
+ thumbInset?: number;
48
+ radius?: number;
49
+ labelGap?: number;
50
+ textInset?: number;
51
+ textSize?: number;
52
+ textLineHeight?: number;
53
+ };
54
+
55
+ export type SwitchColors = {
56
+ checkedTrack?: string;
57
+ uncheckedTrack?: string;
58
+ thumb?: string;
59
+ checkedThumb?: string;
60
+ uncheckedThumb?: string;
61
+ checkedText?: string;
62
+ uncheckedText?: string;
63
+ focusRing?: string;
64
+ loading?: string;
65
+ };
66
+
67
+ export type SwitchSlotProps = {
68
+ checked: boolean;
69
+ disabled: boolean;
70
+ loading: boolean;
71
+ interactive: boolean;
72
+ size: SwitchSize;
73
+ tone: SwitchTone;
74
+ toggle: () => void;
75
+ };
76
+
77
+ type SwitchMetrics = {
78
+ width: number;
79
+ height: number;
80
+ thumbInset: number;
81
+ labelGap: number;
82
+ textInset: number;
83
+ textSize: number;
84
+ textLineHeight: number;
85
+ textSlotMinWidth: number;
86
+ minTouchTarget: number;
87
+ focusRingWidth: number;
88
+ focusRingOffset: number;
89
+ };
90
+
91
+ type SwitchTonePalette = {
92
+ checkedTrack: string;
93
+ checkedText: string;
94
+ };
95
+
96
+ type NativePressableProps = Omit<
97
+ React.ComponentPropsWithoutRef<typeof Pressable>,
98
+ 'accessibilityRole' | 'accessibilityState' | 'children' | 'disabled' | 'style' | 'onChange'
99
+ >;
100
+
101
+ type PressableFocusEvent = Parameters<
102
+ NonNullable<React.ComponentPropsWithoutRef<typeof Pressable>['onFocus']>
103
+ >[0];
104
+ type PressableBlurEvent = Parameters<
105
+ NonNullable<React.ComponentPropsWithoutRef<typeof Pressable>['onBlur']>
106
+ >[0];
107
+
108
+ export type SwitchRef = React.ComponentRef<typeof Pressable>;
109
+
110
+ export interface SwitchProps extends NativePressableProps {
111
+ checked?: boolean;
112
+ defaultChecked?: boolean;
113
+ onCheckedChange?: (checked: boolean) => void;
114
+
115
+ disabled?: boolean;
116
+ loading?: boolean;
117
+ loadingIndicator?: React.ReactNode;
118
+
119
+ size?: SwitchSize;
120
+ tone?: SwitchTone;
121
+ duration?: number;
122
+
123
+ color?: string;
124
+ colors?: SwitchColors;
125
+ layout?: SwitchLayout;
126
+ stateText?: SwitchStateText;
127
+
128
+ label?: React.ReactNode;
129
+ description?: React.ReactNode;
130
+ labelPlacement?: SwitchLabelPlacement;
131
+ children?: React.ReactNode | ((slot: SwitchSlotProps) => React.ReactNode);
132
+
133
+ style?: React.ComponentPropsWithoutRef<typeof Pressable>['style'];
134
+ contentStyle?: StyleProp<ViewStyle>;
135
+ trackStyle?: StyleProp<ViewStyle>;
136
+ thumbStyle?: StyleProp<ViewStyle>;
137
+ labelStyle?: StyleProp<TextStyle>;
138
+ descriptionStyle?: StyleProp<TextStyle>;
139
+ stateTextStyle?: StyleProp<TextStyle>;
140
+ accessibilityState?: React.ComponentPropsWithoutRef<typeof Pressable>['accessibilityState'];
141
+ }
142
+
143
+ const SEMANTIC_COLORS: Record<string, string> = {
144
+ danger: '#DC2626',
145
+ error: '#DC2626',
146
+ info: '#2563EB',
147
+ success: '#16A34A',
148
+ warn: '#D97706',
149
+ warning: '#D97706',
150
+ };
151
+
152
+ const SIZE_TOKENS = {
153
+ sm: {
154
+ width: 44,
155
+ height: 26,
156
+ thumbInset: 2,
157
+ labelGap: 10,
158
+ textInset: 4,
159
+ textSize: 11,
160
+ textLineHeight: 13,
161
+ textSlotMinWidth: 24,
162
+ },
163
+ md: {
164
+ width: 52,
165
+ height: 32,
166
+ thumbInset: 3,
167
+ labelGap: 12,
168
+ textInset: 5,
169
+ textSize: 12,
170
+ textLineHeight: 14,
171
+ textSlotMinWidth: 30,
172
+ },
173
+ lg: {
174
+ width: 64,
175
+ height: 38,
176
+ thumbInset: 3,
177
+ labelGap: 14,
178
+ textInset: 6,
179
+ textSize: 13,
180
+ textLineHeight: 16,
181
+ textSlotMinWidth: 36,
182
+ },
183
+ } as const satisfies Record<
184
+ SwitchSize,
185
+ Omit<SwitchMetrics, 'minTouchTarget' | 'focusRingWidth' | 'focusRingOffset'>
186
+ >;
187
+
188
+ const PRESSED_OPACITY = 0.92;
189
+ const STATE_TEXT_WIDTH_FACTOR = 0.58;
190
+ const STATE_TEXT_WIDTH_GUARD_FACTOR = 0.25;
191
+ const VALUE_TIMING_DURATION = 180;
192
+ const PRESS_IN_DURATION = 90;
193
+ const PRESS_OUT_DURATION = 140;
194
+ const FOCUS_DURATION = 120;
195
+
196
+ function resolveSwitchMetrics(size: SwitchSize): SwitchMetrics {
197
+ const token = SIZE_TOKENS[size] ?? SIZE_TOKENS.md;
198
+ return {
199
+ width: wp(token.width),
200
+ height: wp(token.height),
201
+ thumbInset: wp(token.thumbInset),
202
+ labelGap: wp(token.labelGap),
203
+ textInset: wp(token.textInset),
204
+ textSize: wp(token.textSize),
205
+ textLineHeight: wp(token.textLineHeight),
206
+ textSlotMinWidth: wp(token.textSlotMinWidth),
207
+ minTouchTarget: wp(44),
208
+ focusRingWidth: wp(2),
209
+ focusRingOffset: wp(3),
210
+ };
211
+ }
212
+
213
+ function isProcessableColor(color: string) {
214
+ return processColor(color) != null;
215
+ }
216
+
217
+ function colorToRgba(color: string, alpha: number) {
218
+ const processed = processColor(color);
219
+ if (typeof processed !== 'number') return undefined;
220
+
221
+ const normalized = processed >>> 0;
222
+ const r = (normalized >> 16) & 255;
223
+ const g = (normalized >> 8) & 255;
224
+ const b = normalized & 255;
225
+ const a = Math.max(0, Math.min(1, alpha));
226
+ return `rgba(${r},${g},${b},${a})`;
227
+ }
228
+
229
+ function colorToChannels(color: string) {
230
+ const processed = processColor(color);
231
+ if (typeof processed !== 'number') return undefined;
232
+
233
+ const normalized = processed >>> 0;
234
+ return {
235
+ r: (normalized >> 16) & 255,
236
+ g: (normalized >> 8) & 255,
237
+ b: normalized & 255,
238
+ a: ((normalized >> 24) & 255) / 255,
239
+ };
240
+ }
241
+
242
+ function overlayColor(foreground: string, background: string, opacity: number) {
243
+ const fg = colorToChannels(foreground);
244
+ const bg = colorToChannels(background);
245
+ if (!fg || !bg) return undefined;
246
+
247
+ const fgAlpha = Math.max(0, Math.min(1, opacity)) * fg.a;
248
+ const bgAlpha = bg.a;
249
+ const alpha = fgAlpha + bgAlpha * (1 - fgAlpha);
250
+ if (alpha <= 0) return 'rgba(0,0,0,0)';
251
+
252
+ const r = Math.round((fg.r * fgAlpha + bg.r * bgAlpha * (1 - fgAlpha)) / alpha);
253
+ const g = Math.round((fg.g * fgAlpha + bg.g * bgAlpha * (1 - fgAlpha)) / alpha);
254
+ const b = Math.round((fg.b * fgAlpha + bg.b * bgAlpha * (1 - fgAlpha)) / alpha);
255
+ return `rgba(${r},${g},${b},${Math.round(alpha * 1000) / 1000})`;
256
+ }
257
+
258
+ function resolveColorToken(input: string | undefined, fallback: string, theme: Theme) {
259
+ if (input == null) return fallback;
260
+
261
+ const key = input.trim();
262
+ if (!key) return fallback;
263
+
264
+ const resolved =
265
+ key === 'primary'
266
+ ? theme.colors.primary
267
+ : key === 'onPrimary'
268
+ ? theme.colors.onPrimary
269
+ : key === 'secondary' || key === 'neutral'
270
+ ? theme.colors.secondary
271
+ : key === 'onSecondary'
272
+ ? theme.colors.onSecondary
273
+ : key === 'surface'
274
+ ? theme.colors.surface
275
+ : key === 'onSurface'
276
+ ? theme.colors.onSurface
277
+ : key === 'border'
278
+ ? theme.colors.border
279
+ : key === 'muted'
280
+ ? theme.colors.muted
281
+ : key === 'disabled'
282
+ ? theme.colors.disabled
283
+ : SEMANTIC_COLORS[key] ?? key;
284
+
285
+ return isProcessableColor(resolved) ? resolved : fallback;
286
+ }
287
+
288
+ function resolveTonePalette(tone: SwitchTone, theme: Theme): SwitchTonePalette {
289
+ if (tone === 'neutral') {
290
+ return {
291
+ checkedTrack: theme.colors.onSurface,
292
+ checkedText: theme.colors.surface,
293
+ };
294
+ }
295
+
296
+ if (tone === 'success') {
297
+ return {
298
+ checkedTrack: SEMANTIC_COLORS.success,
299
+ checkedText: '#FFFFFF',
300
+ };
301
+ }
302
+
303
+ if (tone === 'warning') {
304
+ return {
305
+ checkedTrack: SEMANTIC_COLORS.warning,
306
+ checkedText: '#111827',
307
+ };
308
+ }
309
+
310
+ if (tone === 'danger') {
311
+ return {
312
+ checkedTrack: SEMANTIC_COLORS.danger,
313
+ checkedText: '#FFFFFF',
314
+ };
315
+ }
316
+
317
+ if (tone === 'info') {
318
+ return {
319
+ checkedTrack: SEMANTIC_COLORS.info,
320
+ checkedText: '#FFFFFF',
321
+ };
322
+ }
323
+
324
+ return {
325
+ checkedTrack: theme.colors.primary,
326
+ checkedText: theme.colors.onPrimary,
327
+ };
328
+ }
329
+
330
+ function resolveUncheckedTrackFallback(scheme: ReturnType<typeof useColorScheme>, theme: Theme) {
331
+ if (scheme === 'dark') return '#374151';
332
+ return colorToRgba(theme.colors.onSurface, 0.12) ?? theme.colors.border;
333
+ }
334
+
335
+ function clampNumber(value: number, min: number, max: number) {
336
+ if (!Number.isFinite(value)) return min;
337
+ return Math.min(Math.max(value, min), max);
338
+ }
339
+
340
+ function resolvePositiveNumber(value: number | undefined, fallback: number) {
341
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
342
+ }
343
+
344
+ function resolveNonNegativeNumber(value: number | undefined, fallback: number) {
345
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback;
346
+ }
347
+
348
+ function resolveDuration(duration: number | undefined) {
349
+ if (duration == null || !Number.isFinite(duration)) return VALUE_TIMING_DURATION;
350
+ return Math.max(0, duration);
351
+ }
352
+
353
+ function toTimingConfig(duration: number) {
354
+ return {
355
+ duration,
356
+ easing: Easing.out(Easing.cubic),
357
+ reduceMotion: ReduceMotion.System,
358
+ } as const;
359
+ }
360
+
361
+ function resolveHitSlop(width: number, height: number, minTouchTarget: number): Insets | undefined {
362
+ const vertical = Math.max(0, (minTouchTarget - height) / 2);
363
+ const horizontal = Math.max(0, (minTouchTarget - width) / 2);
364
+ if (vertical === 0 && horizontal === 0) return undefined;
365
+ return {
366
+ top: vertical,
367
+ bottom: vertical,
368
+ left: horizontal,
369
+ right: horizontal,
370
+ };
371
+ }
372
+
373
+ function resolveWebCursorStyle(disabled: boolean): ViewStyle | undefined {
374
+ if (Platform.OS !== 'web') return undefined;
375
+ return { cursor: disabled ? 'not-allowed' : 'pointer' } as ViewStyle;
376
+ }
377
+
378
+ function isPrimitiveText(node: React.ReactNode): node is string | number {
379
+ return typeof node === 'string' || typeof node === 'number';
380
+ }
381
+
382
+ function isRenderProp(children: SwitchProps['children']): children is (slot: SwitchSlotProps) => React.ReactNode {
383
+ return typeof children === 'function';
384
+ }
385
+
386
+ function getTextLength(text: string | undefined) {
387
+ return text == null ? 0 : Array.from(text).length;
388
+ }
389
+
390
+ function isWideGlyph(codePoint: number) {
391
+ return (
392
+ (codePoint >= 0x1100 && codePoint <= 0x115f) ||
393
+ (codePoint >= 0x2329 && codePoint <= 0x232a) ||
394
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
395
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
396
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
397
+ (codePoint >= 0xfe10 && codePoint <= 0xfe6f) ||
398
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
399
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
400
+ (codePoint >= 0x1f000 && codePoint <= 0x1faff)
401
+ );
402
+ }
403
+
404
+ function estimateStateTextWidth(text: string | undefined, fontSize: number) {
405
+ const glyphs = Array.from(text ?? '');
406
+ if (glyphs.length === 0) return 0;
407
+
408
+ let widthUnits = 0;
409
+ for (const glyph of glyphs) {
410
+ const codePoint = glyph.codePointAt(0) ?? 0;
411
+ if (isWideGlyph(codePoint)) {
412
+ widthUnits += 1;
413
+ } else if (/\s/.test(glyph)) {
414
+ widthUnits += 0.32;
415
+ } else if (/[ilI.,'`|!]/.test(glyph)) {
416
+ widthUnits += 0.34;
417
+ } else if (/[mwMW@#%&]/.test(glyph)) {
418
+ widthUnits += 0.82;
419
+ } else if (/[A-Z0-9]/.test(glyph)) {
420
+ widthUnits += 0.62;
421
+ } else {
422
+ widthUnits += 0.56;
423
+ }
424
+ }
425
+
426
+ return Math.max(getTextLength(text) * fontSize * STATE_TEXT_WIDTH_FACTOR, widthUnits * fontSize);
427
+ }
428
+
429
+ function SwitchImpl(
430
+ {
431
+ checked: checkedProp,
432
+ defaultChecked = false,
433
+ onCheckedChange,
434
+ disabled = false,
435
+ loading = false,
436
+ loadingIndicator,
437
+ size = 'md',
438
+ tone = 'primary',
439
+ duration,
440
+ color,
441
+ colors,
442
+ layout,
443
+ stateText,
444
+ label,
445
+ description,
446
+ labelPlacement = 'start',
447
+ children,
448
+ style,
449
+ contentStyle,
450
+ trackStyle,
451
+ thumbStyle,
452
+ labelStyle,
453
+ descriptionStyle,
454
+ stateTextStyle,
455
+ accessibilityLabel,
456
+ accessibilityState,
457
+ accessibilityValue,
458
+ hitSlop,
459
+ onPress,
460
+ onPressIn,
461
+ onPressOut,
462
+ onFocus,
463
+ onBlur,
464
+ testID,
465
+ ...pressableProps
466
+ }: SwitchProps,
467
+ ref: React.ForwardedRef<SwitchRef>
468
+ ) {
469
+ const theme = useTheme();
470
+ const scheme = useColorScheme();
471
+ const { width: viewportWidth } = useWindowDimensions();
472
+ const isRTL = I18nManager.isRTL;
473
+
474
+ const isControlled = checkedProp !== undefined;
475
+ const [uncontrolledChecked, setUncontrolledChecked] = React.useState<boolean>(() => defaultChecked);
476
+ const checked = isControlled ? checkedProp! : uncontrolledChecked;
477
+ const checkedRef = React.useRef(checked);
478
+ checkedRef.current = checked;
479
+
480
+ const interactionDisabled = disabled || loading;
481
+ const interactive = !interactionDisabled;
482
+
483
+ const metrics = React.useMemo(() => resolveSwitchMetrics(size), [size, viewportWidth]);
484
+ const tonePalette = React.useMemo(() => resolveTonePalette(tone, theme), [theme, tone]);
485
+
486
+ const checkedTrackColor = React.useMemo(
487
+ () => resolveColorToken(color ?? colors?.checkedTrack, tonePalette.checkedTrack, theme),
488
+ [color, colors?.checkedTrack, theme, tonePalette.checkedTrack]
489
+ );
490
+ const uncheckedTrackColor = React.useMemo(
491
+ () => resolveColorToken(colors?.uncheckedTrack, resolveUncheckedTrackFallback(scheme, theme), theme),
492
+ [colors?.uncheckedTrack, scheme, theme]
493
+ );
494
+ const checkedThumbColor = React.useMemo(
495
+ () => resolveColorToken(colors?.checkedThumb ?? colors?.thumb, theme.colors.surface, theme),
496
+ [colors?.checkedThumb, colors?.thumb, theme]
497
+ );
498
+ const uncheckedThumbColor = React.useMemo(
499
+ () => resolveColorToken(colors?.uncheckedThumb ?? colors?.thumb, theme.colors.surface, theme),
500
+ [colors?.thumb, colors?.uncheckedThumb, theme]
501
+ );
502
+ const checkedTextColor = React.useMemo(
503
+ () => resolveColorToken(colors?.checkedText, tonePalette.checkedText, theme),
504
+ [colors?.checkedText, theme, tonePalette.checkedText]
505
+ );
506
+ const uncheckedTextColor = React.useMemo(
507
+ () =>
508
+ resolveColorToken(
509
+ colors?.uncheckedText,
510
+ scheme === 'dark' ? '#E5E7EB' : theme.colors.muted,
511
+ theme
512
+ ),
513
+ [colors?.uncheckedText, scheme, theme]
514
+ );
515
+ const focusRingColor = React.useMemo(
516
+ () =>
517
+ resolveColorToken(
518
+ colors?.focusRing,
519
+ colorToRgba(checkedTrackColor, scheme === 'dark' ? 0.36 : 0.28) ?? checkedTrackColor,
520
+ theme
521
+ ),
522
+ [checkedTrackColor, colors?.focusRing, scheme, theme]
523
+ );
524
+ const disabledUncheckedTrackColor = React.useMemo(
525
+ () =>
526
+ overlayColor(uncheckedTrackColor, theme.colors.surface, scheme === 'dark' ? 0.68 : 0.56) ??
527
+ uncheckedTrackColor,
528
+ [scheme, theme.colors.surface, uncheckedTrackColor]
529
+ );
530
+ const disabledCheckedTrackColor = React.useMemo(
531
+ () =>
532
+ overlayColor(checkedTrackColor, theme.colors.surface, scheme === 'dark' ? 0.5 : 0.56) ??
533
+ checkedTrackColor,
534
+ [checkedTrackColor, scheme, theme.colors.surface]
535
+ );
536
+ const visualUncheckedTrackColor = disabled ? disabledUncheckedTrackColor : uncheckedTrackColor;
537
+ const visualCheckedTrackColor = disabled ? disabledCheckedTrackColor : checkedTrackColor;
538
+ const disabledUncheckedThumbColor = React.useMemo(
539
+ () =>
540
+ overlayColor(
541
+ uncheckedThumbColor,
542
+ disabledUncheckedTrackColor,
543
+ scheme === 'dark' ? 0.84 : 0.95
544
+ ) ?? uncheckedThumbColor,
545
+ [disabledUncheckedTrackColor, scheme, uncheckedThumbColor]
546
+ );
547
+ const disabledCheckedThumbColor = React.useMemo(
548
+ () =>
549
+ overlayColor(
550
+ checkedThumbColor,
551
+ disabledCheckedTrackColor,
552
+ scheme === 'dark' ? 0.84 : 0.92
553
+ ) ?? checkedThumbColor,
554
+ [checkedThumbColor, disabledCheckedTrackColor, scheme]
555
+ );
556
+ const visualUncheckedThumbColor = disabled ? disabledUncheckedThumbColor : uncheckedThumbColor;
557
+ const visualCheckedThumbColor = disabled ? disabledCheckedThumbColor : checkedThumbColor;
558
+ const visualCheckedTextColor = React.useMemo(
559
+ () =>
560
+ disabled
561
+ ? overlayColor(checkedTextColor, disabledCheckedTrackColor, scheme === 'dark' ? 0.78 : 0.72) ??
562
+ checkedTextColor
563
+ : checkedTextColor,
564
+ [checkedTextColor, disabled, disabledCheckedTrackColor, scheme]
565
+ );
566
+ const visualUncheckedTextColor = React.useMemo(
567
+ () =>
568
+ disabled
569
+ ? overlayColor(uncheckedTextColor, disabledUncheckedTrackColor, scheme === 'dark' ? 0.78 : 0.72) ??
570
+ uncheckedTextColor
571
+ : uncheckedTextColor,
572
+ [disabled, disabledUncheckedTrackColor, scheme, uncheckedTextColor]
573
+ );
574
+ const loadingColor = React.useMemo(
575
+ () =>
576
+ resolveColorToken(
577
+ colors?.loading,
578
+ checked ? visualCheckedTrackColor : theme.colors.muted,
579
+ theme
580
+ ),
581
+ [checked, colors?.loading, theme, visualCheckedTrackColor]
582
+ );
583
+ const thumbColorChanges = visualCheckedThumbColor !== visualUncheckedThumbColor;
584
+
585
+ const trackHeight = resolvePositiveNumber(layout?.height, metrics.height);
586
+ const baseTrackWidth = Math.max(trackHeight, resolvePositiveNumber(layout?.width, metrics.width));
587
+ const onePx = wp(1);
588
+ const resolvedThumbInset = clampNumber(
589
+ resolveNonNegativeNumber(layout?.thumbInset, metrics.thumbInset),
590
+ 0,
591
+ Math.max(0, (trackHeight - onePx) / 2)
592
+ );
593
+ const thumbSize = Math.max(onePx, trackHeight - resolvedThumbInset * 2);
594
+ const stateTextSize = resolvePositiveNumber(layout?.textSize, metrics.textSize);
595
+ const stateTextLineHeight = resolvePositiveNumber(layout?.textLineHeight, metrics.textLineHeight);
596
+ const stateTextInset = resolveNonNegativeNumber(layout?.textInset, metrics.textInset);
597
+ const labelGap = resolveNonNegativeNumber(layout?.labelGap, metrics.labelGap);
598
+ const hasStateText = stateText?.checked != null || stateText?.unchecked != null;
599
+ const maxStateTextWidth = Math.max(
600
+ estimateStateTextWidth(stateText?.checked, stateTextSize),
601
+ estimateStateTextWidth(stateText?.unchecked, stateTextSize)
602
+ );
603
+ const stateTextSlotWidth = hasStateText
604
+ ? Math.max(
605
+ metrics.textSlotMinWidth,
606
+ maxStateTextWidth + stateTextSize * STATE_TEXT_WIDTH_GUARD_FACTOR + stateTextInset * 2
607
+ )
608
+ : 0;
609
+ const trackWidth = hasStateText
610
+ ? Math.max(baseTrackWidth, thumbSize + resolvedThumbInset + stateTextSlotWidth)
611
+ : baseTrackWidth;
612
+ const travel = Math.max(0, trackWidth - thumbSize - resolvedThumbInset * 2);
613
+ const thumbStart = isRTL ? trackWidth - thumbSize - resolvedThumbInset : resolvedThumbInset;
614
+ const travelDirection = isRTL ? -1 : 1;
615
+ const trackRadius = Math.max(0, layout?.radius ?? trackHeight / 2);
616
+ const thumbRadius = thumbSize / 2;
617
+ const defaultHitSlop = React.useMemo(
618
+ () => resolveHitSlop(trackWidth, trackHeight, metrics.minTouchTarget),
619
+ [metrics.minTouchTarget, trackHeight, trackWidth]
620
+ );
621
+ const thumbShadowStyle = React.useMemo<ViewStyle>(
622
+ () => ({
623
+ shadowColor: '#000000',
624
+ shadowOffset: { width: 0, height: wp(1) },
625
+ shadowOpacity: disabled ? 0 : scheme === 'dark' ? 0.28 : 0.14,
626
+ shadowRadius: disabled ? 0 : wp(4),
627
+ elevation: disabled ? 0 : wp(1),
628
+ }),
629
+ [disabled, scheme]
630
+ );
631
+ const webCursorStyle = React.useMemo(
632
+ () => resolveWebCursorStyle(interactionDisabled),
633
+ [interactionDisabled]
634
+ );
635
+
636
+ const resolvedDuration = resolveDuration(duration);
637
+ const valueTiming = React.useMemo(() => toTimingConfig(resolvedDuration), [resolvedDuration]);
638
+ const pressInTiming = React.useMemo(() => toTimingConfig(PRESS_IN_DURATION), []);
639
+ const pressOutTiming = React.useMemo(() => toTimingConfig(PRESS_OUT_DURATION), []);
640
+ const focusTiming = React.useMemo(() => toTimingConfig(FOCUS_DURATION), []);
641
+
642
+ const progressSv = useSharedValue(checked ? 1 : 0);
643
+ const pressSv = useSharedValue(0);
644
+ const focusSv = useSharedValue(0);
645
+
646
+ React.useEffect(() => {
647
+ if (!isControlled) return;
648
+ progressSv.value = withTiming(checked ? 1 : 0, valueTiming);
649
+ }, [checked, isControlled, progressSv, valueTiming]);
650
+
651
+ React.useEffect(() => {
652
+ if (interactive) return;
653
+ pressSv.value = withTiming(0, pressOutTiming);
654
+ focusSv.value = withTiming(0, focusTiming);
655
+ }, [focusSv, focusTiming, interactive, pressOutTiming, pressSv]);
656
+
657
+ const commitChecked = React.useCallback(
658
+ (next: boolean) => {
659
+ if (!isControlled) {
660
+ checkedRef.current = next;
661
+ progressSv.value = withTiming(next ? 1 : 0, valueTiming);
662
+ setUncontrolledChecked(next);
663
+ }
664
+ onCheckedChange?.(next);
665
+ },
666
+ [isControlled, onCheckedChange, progressSv, valueTiming]
667
+ );
668
+
669
+ const toggle = React.useCallback(() => {
670
+ if (interactionDisabled) return;
671
+ commitChecked(!checkedRef.current);
672
+ }, [commitChecked, interactionDisabled]);
673
+
674
+ const handlePress = React.useCallback(
675
+ (event: GestureResponderEvent) => {
676
+ if (!interactionDisabled) {
677
+ commitChecked(!checkedRef.current);
678
+ }
679
+ onPress?.(event);
680
+ },
681
+ [commitChecked, interactionDisabled, onPress]
682
+ );
683
+
684
+ const handlePressIn = React.useCallback(
685
+ (event: GestureResponderEvent) => {
686
+ if (!interactionDisabled) {
687
+ pressSv.value = withTiming(1, pressInTiming);
688
+ }
689
+ onPressIn?.(event);
690
+ },
691
+ [interactionDisabled, onPressIn, pressInTiming, pressSv]
692
+ );
693
+
694
+ const handlePressOut = React.useCallback(
695
+ (event: GestureResponderEvent) => {
696
+ pressSv.value = withTiming(0, pressOutTiming);
697
+ onPressOut?.(event);
698
+ },
699
+ [onPressOut, pressOutTiming, pressSv]
700
+ );
701
+
702
+ const handleFocus = React.useCallback(
703
+ (event: PressableFocusEvent) => {
704
+ if (!interactionDisabled) {
705
+ focusSv.value = withTiming(1, focusTiming);
706
+ }
707
+ onFocus?.(event);
708
+ },
709
+ [focusSv, focusTiming, interactionDisabled, onFocus]
710
+ );
711
+
712
+ const handleBlur = React.useCallback(
713
+ (event: PressableBlurEvent) => {
714
+ focusSv.value = withTiming(0, focusTiming);
715
+ onBlur?.(event);
716
+ },
717
+ [focusSv, focusTiming, onBlur]
718
+ );
719
+
720
+ const visualAnimatedStyle = useAnimatedStyle(() => {
721
+ const pressedOpacity = interpolate(pressSv.value, [0, 1], [1, PRESSED_OPACITY]);
722
+ return { opacity: pressedOpacity };
723
+ });
724
+
725
+ const focusRingAnimatedStyle = useAnimatedStyle(() => {
726
+ const opacity = interpolate(focusSv.value, [0, 1], [0, 1]);
727
+ const scale = interpolate(focusSv.value, [0, 1], [0.96, 1]);
728
+ return {
729
+ opacity,
730
+ transform: [{ scale }],
731
+ };
732
+ });
733
+
734
+ const checkedTrackAnimatedStyle = useAnimatedStyle(() => {
735
+ return { opacity: progressSv.value };
736
+ });
737
+
738
+ const thumbAnimatedStyle = useAnimatedStyle(() => {
739
+ if (!thumbColorChanges) {
740
+ return {
741
+ transform: [{ translateX: travel * travelDirection * progressSv.value }],
742
+ };
743
+ }
744
+
745
+ return {
746
+ backgroundColor: interpolateColor(
747
+ progressSv.value,
748
+ [0, 1],
749
+ [visualUncheckedThumbColor, visualCheckedThumbColor]
750
+ ),
751
+ transform: [{ translateX: travel * travelDirection * progressSv.value }],
752
+ };
753
+ }, [thumbColorChanges, travel, travelDirection, visualCheckedThumbColor, visualUncheckedThumbColor]);
754
+
755
+ const checkedTextAnimatedStyle = useAnimatedStyle(() => {
756
+ return { opacity: progressSv.value };
757
+ });
758
+
759
+ const uncheckedTextAnimatedStyle = useAnimatedStyle(() => {
760
+ return { opacity: 1 - progressSv.value };
761
+ });
762
+
763
+ const checkedTextSlotStyle = React.useMemo<ViewStyle>(
764
+ () =>
765
+ isRTL
766
+ ? {
767
+ left: thumbSize + resolvedThumbInset,
768
+ right: 0,
769
+ paddingLeft: stateTextInset,
770
+ paddingRight: stateTextInset,
771
+ }
772
+ : {
773
+ left: 0,
774
+ right: thumbSize + resolvedThumbInset,
775
+ paddingLeft: stateTextInset,
776
+ paddingRight: stateTextInset,
777
+ },
778
+ [isRTL, resolvedThumbInset, stateTextInset, thumbSize]
779
+ );
780
+ const uncheckedTextSlotStyle = React.useMemo<ViewStyle>(
781
+ () =>
782
+ isRTL
783
+ ? {
784
+ left: 0,
785
+ right: thumbSize + resolvedThumbInset,
786
+ paddingLeft: stateTextInset,
787
+ paddingRight: stateTextInset,
788
+ }
789
+ : {
790
+ left: thumbSize + resolvedThumbInset,
791
+ right: 0,
792
+ paddingLeft: stateTextInset,
793
+ paddingRight: stateTextInset,
794
+ },
795
+ [isRTL, resolvedThumbInset, stateTextInset, thumbSize]
796
+ );
797
+
798
+ const slot = React.useMemo<SwitchSlotProps>(
799
+ () => ({
800
+ checked,
801
+ disabled: interactionDisabled,
802
+ loading,
803
+ interactive,
804
+ size,
805
+ tone,
806
+ toggle,
807
+ }),
808
+ [checked, interactionDisabled, interactive, loading, size, tone, toggle]
809
+ );
810
+
811
+ const renderedContent = React.useMemo(() => {
812
+ if (isRenderProp(children)) return children(slot);
813
+ if (children != null) return children;
814
+ if (label == null && description == null) return null;
815
+
816
+ return (
817
+ <View style={styles.copy}>
818
+ {label != null ? (
819
+ isPrimitiveText(label) ? (
820
+ <Text
821
+ disabled={interactionDisabled}
822
+ numberOfLines={1}
823
+ variant="label"
824
+ style={labelStyle}
825
+ >
826
+ {label}
827
+ </Text>
828
+ ) : (
829
+ label
830
+ )
831
+ ) : null}
832
+ {description != null ? (
833
+ isPrimitiveText(description) ? (
834
+ <Text
835
+ disabled={interactionDisabled}
836
+ numberOfLines={2}
837
+ tone={interactionDisabled ? 'disabled' : 'muted'}
838
+ variant="caption"
839
+ style={descriptionStyle}
840
+ >
841
+ {description}
842
+ </Text>
843
+ ) : (
844
+ description
845
+ )
846
+ ) : null}
847
+ </View>
848
+ );
849
+ }, [children, description, descriptionStyle, interactionDisabled, label, labelStyle, slot]);
850
+
851
+ const hasContent = renderedContent != null;
852
+ const inferredAccessibilityLabel = isPrimitiveText(label) ? String(label) : undefined;
853
+ const resolvedAccessibilityValue =
854
+ accessibilityValue ??
855
+ (hasStateText
856
+ ? {
857
+ text: checked ? (stateText?.checked ?? '') : (stateText?.unchecked ?? ''),
858
+ }
859
+ : undefined);
860
+ const resolvedStyle = React.useCallback(
861
+ (state: PressableStateCallbackType) => {
862
+ const userStyle = typeof style === 'function' ? style(state) : style;
863
+ return [styles.root, webCursorStyle, userStyle];
864
+ },
865
+ [style, webCursorStyle]
866
+ );
867
+
868
+ const switchNode = (
869
+ <Animated.View
870
+ style={[
871
+ styles.visual,
872
+ {
873
+ width: trackWidth,
874
+ height: trackHeight,
875
+ borderRadius: trackRadius,
876
+ },
877
+ visualAnimatedStyle,
878
+ ]}
879
+ >
880
+ <Animated.View
881
+ pointerEvents="none"
882
+ style={[
883
+ styles.focusRing,
884
+ {
885
+ top: -metrics.focusRingOffset,
886
+ bottom: -metrics.focusRingOffset,
887
+ left: -metrics.focusRingOffset,
888
+ right: -metrics.focusRingOffset,
889
+ borderWidth: metrics.focusRingWidth,
890
+ borderRadius: trackRadius + metrics.focusRingOffset,
891
+ borderColor: focusRingColor,
892
+ },
893
+ focusRingAnimatedStyle,
894
+ ]}
895
+ />
896
+ <View
897
+ style={[
898
+ StyleSheet.absoluteFillObject,
899
+ {
900
+ borderRadius: trackRadius,
901
+ backgroundColor: visualUncheckedTrackColor,
902
+ },
903
+ trackStyle,
904
+ ]}
905
+ />
906
+ <Animated.View
907
+ pointerEvents="none"
908
+ style={[
909
+ StyleSheet.absoluteFillObject,
910
+ {
911
+ borderRadius: trackRadius,
912
+ backgroundColor: visualCheckedTrackColor,
913
+ },
914
+ checkedTrackAnimatedStyle,
915
+ ]}
916
+ />
917
+
918
+ {hasStateText ? (
919
+ <View pointerEvents="none" style={styles.stateTextLayer}>
920
+ <Animated.View style={[styles.stateTextSlot, checkedTextSlotStyle, checkedTextAnimatedStyle]}>
921
+ <Text
922
+ allowFontScaling={false}
923
+ ellipsizeMode="clip"
924
+ numberOfLines={1}
925
+ style={[
926
+ styles.stateText,
927
+ {
928
+ color: visualCheckedTextColor,
929
+ fontSize: stateTextSize,
930
+ lineHeight: stateTextLineHeight,
931
+ },
932
+ stateTextStyle,
933
+ ]}
934
+ >
935
+ {stateText?.checked ?? ''}
936
+ </Text>
937
+ </Animated.View>
938
+ <Animated.View style={[styles.stateTextSlot, uncheckedTextSlotStyle, uncheckedTextAnimatedStyle]}>
939
+ <Text
940
+ allowFontScaling={false}
941
+ ellipsizeMode="clip"
942
+ numberOfLines={1}
943
+ style={[
944
+ styles.stateText,
945
+ {
946
+ color: visualUncheckedTextColor,
947
+ fontSize: stateTextSize,
948
+ lineHeight: stateTextLineHeight,
949
+ },
950
+ stateTextStyle,
951
+ ]}
952
+ >
953
+ {stateText?.unchecked ?? ''}
954
+ </Text>
955
+ </Animated.View>
956
+ </View>
957
+ ) : null}
958
+
959
+ <Animated.View
960
+ style={[
961
+ styles.thumb,
962
+ {
963
+ width: thumbSize,
964
+ height: thumbSize,
965
+ left: thumbStart,
966
+ top: resolvedThumbInset,
967
+ borderRadius: thumbRadius,
968
+ backgroundColor: visualUncheckedThumbColor,
969
+ },
970
+ thumbAnimatedStyle,
971
+ thumbShadowStyle,
972
+ thumbStyle,
973
+ ]}
974
+ >
975
+ {loading ? (
976
+ loadingIndicator ?? (
977
+ <LoadingSpinner
978
+ size={Math.max(wp(12), thumbSize * 0.58)}
979
+ color={loadingColor}
980
+ />
981
+ )
982
+ ) : null}
983
+ </Animated.View>
984
+ </Animated.View>
985
+ );
986
+
987
+ const contentNode = hasContent ? (
988
+ <View
989
+ style={[
990
+ styles.content,
991
+ contentStyle,
992
+ ]}
993
+ >
994
+ {labelPlacement === 'start' ? (
995
+ <>
996
+ <View style={[styles.contentSlot, { marginEnd: labelGap }]}>{renderedContent}</View>
997
+ {switchNode}
998
+ </>
999
+ ) : (
1000
+ <>
1001
+ {switchNode}
1002
+ <View style={[styles.contentSlot, { marginStart: labelGap }]}>{renderedContent}</View>
1003
+ </>
1004
+ )}
1005
+ </View>
1006
+ ) : (
1007
+ switchNode
1008
+ );
1009
+
1010
+ return (
1011
+ <Pressable
1012
+ {...pressableProps}
1013
+ ref={ref}
1014
+ accessibilityLabel={accessibilityLabel ?? inferredAccessibilityLabel}
1015
+ accessibilityRole="switch"
1016
+ accessibilityState={{
1017
+ ...accessibilityState,
1018
+ checked,
1019
+ disabled: Boolean(interactionDisabled),
1020
+ busy: Boolean(loading || accessibilityState?.busy),
1021
+ }}
1022
+ accessibilityValue={resolvedAccessibilityValue}
1023
+ disabled={interactionDisabled}
1024
+ hitSlop={hitSlop ?? defaultHitSlop}
1025
+ onBlur={handleBlur}
1026
+ onFocus={handleFocus}
1027
+ onPress={handlePress}
1028
+ onPressIn={handlePressIn}
1029
+ onPressOut={handlePressOut}
1030
+ style={resolvedStyle}
1031
+ testID={testID}
1032
+ >
1033
+ {contentNode}
1034
+ </Pressable>
1035
+ );
1036
+ }
1037
+
1038
+ const SwitchWithRef = React.forwardRef<SwitchRef, SwitchProps>(SwitchImpl);
1039
+ SwitchWithRef.displayName = 'Switch';
1040
+
1041
+ export const Switch = React.memo(SwitchWithRef);
1042
+ Switch.displayName = 'Switch';
1043
+
1044
+ const styles = StyleSheet.create({
1045
+ root: {
1046
+ alignSelf: 'flex-start',
1047
+ },
1048
+ content: {
1049
+ alignItems: 'center',
1050
+ flexDirection: 'row',
1051
+ minWidth: 0,
1052
+ },
1053
+ contentSlot: {
1054
+ flexGrow: 1,
1055
+ flexShrink: 1,
1056
+ minWidth: 0,
1057
+ },
1058
+ copy: {
1059
+ flexShrink: 1,
1060
+ minWidth: 0,
1061
+ },
1062
+ visual: {
1063
+ flexShrink: 0,
1064
+ overflow: 'visible',
1065
+ },
1066
+ focusRing: {
1067
+ position: 'absolute',
1068
+ },
1069
+ stateTextLayer: {
1070
+ ...StyleSheet.absoluteFillObject,
1071
+ alignItems: 'center',
1072
+ justifyContent: 'center',
1073
+ },
1074
+ stateTextSlot: {
1075
+ ...StyleSheet.absoluteFillObject,
1076
+ alignItems: 'center',
1077
+ justifyContent: 'center',
1078
+ overflow: 'hidden',
1079
+ },
1080
+ stateText: {
1081
+ includeFontPadding: false,
1082
+ fontWeight: '600',
1083
+ margin: 0,
1084
+ padding: 0,
1085
+ textAlign: 'center',
1086
+ },
1087
+ thumb: {
1088
+ alignItems: 'center',
1089
+ justifyContent: 'center',
1090
+ position: 'absolute',
1091
+ },
1092
+ });