@expokit/cli 0.0.0-stage → 0.2.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/bin/expo-kit.js +1204 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +338 -0
  6. package/package.json +44 -4
  7. package/registry/_docs/accordion.json +99 -0
  8. package/registry/_docs/action-sheet.json +110 -0
  9. package/registry/_docs/alert.json +127 -0
  10. package/registry/_docs/avatar.json +203 -0
  11. package/registry/_docs/badge.json +117 -0
  12. package/registry/_docs/bottom-nav.json +119 -0
  13. package/registry/_docs/bottom-sheet.json +158 -0
  14. package/registry/_docs/bullet.json +24 -0
  15. package/registry/_docs/button-3d.json +122 -0
  16. package/registry/_docs/button.json +115 -0
  17. package/registry/_docs/calendar.json +182 -0
  18. package/registry/_docs/card-stack.json +108 -0
  19. package/registry/_docs/card.json +111 -0
  20. package/registry/_docs/carousel.json +102 -0
  21. package/registry/_docs/checkbox.json +93 -0
  22. package/registry/_docs/checkout.json +102 -0
  23. package/registry/_docs/chip-group.json +116 -0
  24. package/registry/_docs/color-swatch.json +66 -0
  25. package/registry/_docs/confetti.json +82 -0
  26. package/registry/_docs/coupon.json +176 -0
  27. package/registry/_docs/cube-pager.json +84 -0
  28. package/registry/_docs/dashboard.json +120 -0
  29. package/registry/_docs/date-picker.json +249 -0
  30. package/registry/_docs/dialog.json +162 -0
  31. package/registry/_docs/drawer.json +171 -0
  32. package/registry/_docs/fab.json +120 -0
  33. package/registry/_docs/flip-card.json +111 -0
  34. package/registry/_docs/floating-input.json +191 -0
  35. package/registry/_docs/gift-card.json +108 -0
  36. package/registry/_docs/glass-button.json +118 -0
  37. package/registry/_docs/glass-nav-bar.json +78 -0
  38. package/registry/_docs/glass-search-bar.json +90 -0
  39. package/registry/_docs/glass-segmented.json +66 -0
  40. package/registry/_docs/glass-sheet.json +108 -0
  41. package/registry/_docs/glass-tab-bar.json +76 -0
  42. package/registry/_docs/glass.json +78 -0
  43. package/registry/_docs/inbox.json +72 -0
  44. package/registry/_docs/isometric-bar.json +72 -0
  45. package/registry/_docs/kit.json +11 -0
  46. package/registry/_docs/kpi-card.json +11 -0
  47. package/registry/_docs/like-button.json +72 -0
  48. package/registry/_docs/liquidglass-tab.json +27 -0
  49. package/registry/_docs/list-item.json +200 -0
  50. package/registry/_docs/loyalty-card.json +108 -0
  51. package/registry/_docs/morph-button.json +108 -0
  52. package/registry/_docs/number-ticker.json +96 -0
  53. package/registry/_docs/offers.json +78 -0
  54. package/registry/_docs/onboarding.json +66 -0
  55. package/registry/_docs/otp-input.json +134 -0
  56. package/registry/_docs/pagination-dots.json +88 -0
  57. package/registry/_docs/parallax-layers.json +84 -0
  58. package/registry/_docs/parallax-scroll.json +186 -0
  59. package/registry/_docs/password-input.json +92 -0
  60. package/registry/_docs/payment-card.json +153 -0
  61. package/registry/_docs/pixel-bar.json +66 -0
  62. package/registry/_docs/polar-area.json +78 -0
  63. package/registry/_docs/profile.json +84 -0
  64. package/registry/_docs/progress-steps.json +92 -0
  65. package/registry/_docs/progress.json +132 -0
  66. package/registry/_docs/radio-group.json +92 -0
  67. package/registry/_docs/rating.json +90 -0
  68. package/registry/_docs/ring-carousel.json +102 -0
  69. package/registry/_docs/scratch-card.json +90 -0
  70. package/registry/_docs/search-bar.json +140 -0
  71. package/registry/_docs/search-results.json +120 -0
  72. package/registry/_docs/segmented-control.json +103 -0
  73. package/registry/_docs/select.json +252 -0
  74. package/registry/_docs/settings.json +72 -0
  75. package/registry/_docs/shimmer-button.json +78 -0
  76. package/registry/_docs/sign-in.json +198 -0
  77. package/registry/_docs/skeleton.json +142 -0
  78. package/registry/_docs/slide-to-confirm.json +84 -0
  79. package/registry/_docs/slider.json +157 -0
  80. package/registry/_docs/spin-wheel.json +114 -0
  81. package/registry/_docs/spinner.json +91 -0
  82. package/registry/_docs/stepper.json +168 -0
  83. package/registry/_docs/swipe-deck.json +102 -0
  84. package/registry/_docs/swipeable-row.json +72 -0
  85. package/registry/_docs/switch.json +104 -0
  86. package/registry/_docs/tabs.json +105 -0
  87. package/registry/_docs/tag-input.json +128 -0
  88. package/registry/_docs/textarea.json +175 -0
  89. package/registry/_docs/ticket-card.json +174 -0
  90. package/registry/_docs/ticket-shape.json +11 -0
  91. package/registry/_docs/tilt-card.json +141 -0
  92. package/registry/_docs/time-picker.json +253 -0
  93. package/registry/_docs/timeline-chart.json +30 -0
  94. package/registry/_docs/timeline.json +156 -0
  95. package/registry/_docs/toast.json +161 -0
  96. package/registry/_docs/treemap.json +30 -0
  97. package/registry/_docs/waterfall.json +36 -0
  98. package/registry/_docs/wheel-list.json +102 -0
  99. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  100. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  101. package/registry/accordion.json +17 -0
  102. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  103. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  104. package/registry/action-sheet.json +18 -0
  105. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  106. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  107. package/registry/alert/files/src/lib/kit.tsx +219 -0
  108. package/registry/alert.json +20 -0
  109. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  110. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  111. package/registry/avatar.json +18 -0
  112. package/registry/badge/files/src/components/ui/badge.tsx +316 -0
  113. package/registry/badge/files/src/lib/kit.tsx +219 -0
  114. package/registry/badge.json +17 -0
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  117. package/registry/bottom-nav.json +18 -0
  118. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  119. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-sheet.json +17 -0
  121. package/registry/bullet.json +14 -0
  122. package/registry/button/files/src/components/ui/button.tsx +274 -0
  123. package/registry/button/files/src/lib/kit.tsx +219 -0
  124. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  125. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  126. package/registry/button-3d.json +18 -0
  127. package/registry/button.json +18 -0
  128. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  129. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  130. package/registry/calendar.json +17 -0
  131. package/registry/card/files/src/components/ui/card.tsx +300 -0
  132. package/registry/card/files/src/lib/kit.tsx +219 -0
  133. package/registry/card-stack.json +15 -0
  134. package/registry/card.json +17 -0
  135. package/registry/carousel.json +15 -0
  136. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  137. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  138. package/registry/checkbox.json +17 -0
  139. package/registry/checkout.json +26 -0
  140. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  141. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  142. package/registry/chip-group.json +17 -0
  143. package/registry/color-swatch.json +17 -0
  144. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  145. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  146. package/registry/confetti.json +15 -0
  147. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  148. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  149. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  150. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  151. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  152. package/registry/coupon.json +23 -0
  153. package/registry/cube-pager.json +15 -0
  154. package/registry/dashboard.json +32 -0
  155. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  156. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  157. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  158. package/registry/date-picker.json +19 -0
  159. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  160. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  161. package/registry/dialog.json +18 -0
  162. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  163. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  164. package/registry/drawer.json +18 -0
  165. package/registry/fab.json +18 -0
  166. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  167. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  168. package/registry/flip-card.json +15 -0
  169. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  170. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  171. package/registry/floating-input.json +17 -0
  172. package/registry/gift-card.json +21 -0
  173. package/registry/glass-button.json +22 -0
  174. package/registry/glass-nav-bar.json +21 -0
  175. package/registry/glass-search-bar.json +21 -0
  176. package/registry/glass-segmented.json +22 -0
  177. package/registry/glass-sheet.json +22 -0
  178. package/registry/glass-tab-bar.json +22 -0
  179. package/registry/glass.json +19 -0
  180. package/registry/inbox.json +30 -0
  181. package/registry/isometric-bar.json +14 -0
  182. package/registry/kit/files/src/lib/kit.tsx +219 -0
  183. package/registry/kit.json +11 -0
  184. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  185. package/registry/kpi-card.json +14 -0
  186. package/registry/like-button.json +18 -0
  187. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  188. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  189. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  190. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  192. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  193. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  194. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  195. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  196. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  197. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  198. package/registry/liquidglass-tab.json +23 -0
  199. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  200. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  201. package/registry/list-item.json +17 -0
  202. package/registry/loyalty-card.json +23 -0
  203. package/registry/morph-button.json +18 -0
  204. package/registry/number-ticker.json +15 -0
  205. package/registry/offers.json +24 -0
  206. package/registry/onboarding.json +17 -0
  207. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  208. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  209. package/registry/otp-input.json +17 -0
  210. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  211. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  212. package/registry/pagination-dots.json +15 -0
  213. package/registry/parallax-layers.json +15 -0
  214. package/registry/parallax-scroll.json +17 -0
  215. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  216. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  217. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  218. package/registry/password-input.json +19 -0
  219. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  220. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  221. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  222. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  223. package/registry/payment-card.json +21 -0
  224. package/registry/pixel-bar.json +14 -0
  225. package/registry/polar-area.json +14 -0
  226. package/registry/profile.json +17 -0
  227. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  228. package/registry/progress/files/src/lib/kit.tsx +219 -0
  229. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  230. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  231. package/registry/progress-steps.json +18 -0
  232. package/registry/progress.json +17 -0
  233. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  234. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  235. package/registry/radio-group.json +18 -0
  236. package/registry/rating.json +18 -0
  237. package/registry/ring-carousel.json +15 -0
  238. package/registry/scratch-card.json +18 -0
  239. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  240. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  241. package/registry/search-bar.json +17 -0
  242. package/registry/search-results.json +30 -0
  243. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  244. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  245. package/registry/segmented-control.json +18 -0
  246. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  247. package/registry/select/files/src/lib/kit.tsx +219 -0
  248. package/registry/select.json +17 -0
  249. package/registry/settings.json +26 -0
  250. package/registry/shimmer-button.json +17 -0
  251. package/registry/sign-in.json +26 -0
  252. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  253. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  254. package/registry/skeleton.json +17 -0
  255. package/registry/slide-to-confirm.json +18 -0
  256. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  257. package/registry/slider/files/src/lib/kit.tsx +219 -0
  258. package/registry/slider.json +17 -0
  259. package/registry/spin-wheel.json +18 -0
  260. package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
  261. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  262. package/registry/spinner.json +17 -0
  263. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  264. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  265. package/registry/stepper.json +17 -0
  266. package/registry/swipe-deck.json +15 -0
  267. package/registry/swipeable-row.json +15 -0
  268. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  269. package/registry/switch/files/src/lib/kit.tsx +219 -0
  270. package/registry/switch.json +18 -0
  271. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  272. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  273. package/registry/tabs.json +17 -0
  274. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  275. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  276. package/registry/tag-input.json +17 -0
  277. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  278. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  279. package/registry/textarea.json +17 -0
  280. package/registry/ticket-card.json +24 -0
  281. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  282. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  283. package/registry/ticket-shape.json +17 -0
  284. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  285. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  286. package/registry/tilt-card.json +17 -0
  287. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  288. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  289. package/registry/time-picker.json +17 -0
  290. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  291. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  292. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  293. package/registry/timeline-chart.json +14 -0
  294. package/registry/timeline.json +20 -0
  295. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  296. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  297. package/registry/toast/files/src/lib/kit.tsx +219 -0
  298. package/registry/toast.json +20 -0
  299. package/registry/treemap.json +14 -0
  300. package/registry/waterfall.json +14 -0
  301. package/registry/wheel-list.json +17 -0
  302. package/schema/components.schema.json +13 -0
@@ -0,0 +1,380 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ Text,
9
+ TextInput,
10
+ View,
11
+ type StyleProp,
12
+ type ViewStyle,
13
+ } from 'react-native';
14
+ import Svg, { Circle, Path } from 'react-native-svg';
15
+
16
+ import { alpha, motion, useKit } from '@/lib/kit';
17
+
18
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
19
+ const CHECK_LEN = 16;
20
+
21
+ export type OtpInputVariant = 'box' | 'underline' | 'pill';
22
+
23
+ export interface OtpInputProps {
24
+ /** Number of cells (default 6). */
25
+ length?: number;
26
+ value?: string;
27
+ onChangeText?: (code: string) => void;
28
+ /** Fires once every cell is filled (also after pasting a full code). */
29
+ onComplete?: (code: string) => void;
30
+ /** Shakes the row and turns it the danger colour. */
31
+ error?: boolean;
32
+ /** Message shown under the cells while `error` is set. */
33
+ errorText?: string;
34
+ /** Cells turn the success colour with a staggered pulse. */
35
+ success?: boolean;
36
+ /** Message shown under the cells while `success` is set. */
37
+ successText?: string;
38
+ /** Hint shown under the cells otherwise. */
39
+ helperText?: string;
40
+ /** Show dots instead of digits. */
41
+ secure?: boolean;
42
+ autoFocus?: boolean;
43
+ disabled?: boolean;
44
+ /** Active cell colour (default: theme accent). */
45
+ accent?: string;
46
+ /** 'box' cells, 'underline' bars, or round 'pill' cells. */
47
+ variant?: OtpInputVariant;
48
+ /** Draws a dash after this many cells, e.g. 3 → `123 – 456`. */
49
+ separatorAfter?: number;
50
+ /** Largest cell width in points; cells shrink below it to fit the row (default 52). */
51
+ maxCellSize?: number;
52
+ style?: StyleProp<ViewStyle>;
53
+ }
54
+
55
+ type CellState = 'idle' | 'error' | 'success';
56
+
57
+ const GAP = 8;
58
+ /** react-native-web draws the browser focus outline; our border + halo is the focus indicator. */
59
+ const webNoOutline = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWidth: 0 } as object) : null;
60
+ const SEP = 22;
61
+
62
+ /**
63
+ * One-time-code field. Cells size themselves to the available width, the active cell glows
64
+ * with a focus halo and a blinking caret, digits pop in, a pasted code fills every cell,
65
+ * errors shake the row and success pulses across the cells.
66
+ */
67
+ export function OtpInput({
68
+ length = 6,
69
+ value,
70
+ onChangeText,
71
+ onComplete,
72
+ error,
73
+ errorText,
74
+ success,
75
+ successText,
76
+ helperText,
77
+ secure,
78
+ autoFocus,
79
+ disabled,
80
+ accent,
81
+ variant = 'box',
82
+ separatorAfter,
83
+ maxCellSize = 52,
84
+ style,
85
+ }: OtpInputProps) {
86
+ const { colors } = useKit();
87
+ const input = useRef<TextInput>(null);
88
+ const [inner, setInner] = useState('');
89
+ const [focused, setFocused] = useState(false);
90
+ const [width, setWidth] = useState(0);
91
+ const code = (value ?? inner).slice(0, length);
92
+ const shake = useRef(new Animated.Value(0)).current;
93
+
94
+ useEffect(() => {
95
+ if (!error) return;
96
+ shake.setValue(0);
97
+ Animated.sequence(
98
+ [10, -9, 7, -5, 3, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 45, useNativeDriver: true }))
99
+ ).start();
100
+ }, [error, shake]);
101
+
102
+ const change = (raw: string) => {
103
+ // No maxLength on the input, so pasting "123-456" or a longer SMS snippet still fills every cell.
104
+ const next = raw.replace(/\D/g, '').slice(0, length);
105
+ if (next === code) return;
106
+ if (value === undefined) setInner(next);
107
+ onChangeText?.(next);
108
+ if (next.length === length && code.length !== length) onComplete?.(next);
109
+ };
110
+
111
+ const sep = separatorAfter && separatorAfter > 0 && separatorAfter < length ? separatorAfter : 0;
112
+ // 4pt each side leaves room for the focus halo.
113
+ const free = width - 8 - GAP * (length - 1) - (sep ? SEP : 0);
114
+ const cell = width ? Math.max(24, Math.min(maxCellSize, Math.floor(free / length))) : maxCellSize;
115
+ const cellH = variant === 'pill' ? cell : variant === 'underline' ? Math.round(cell * 1.04) : Math.round(cell * 1.16);
116
+
117
+ const state: CellState = error ? 'error' : success ? 'success' : 'idle';
118
+ const tone = error ? colors.danger : success ? colors.success : accent ?? colors.accent;
119
+ const activeIndex = Math.min(code.length, length - 1);
120
+ const a11yLabel = `One-time code, ${length} digits`;
121
+ const message = error ? errorText : success ? successText : helperText;
122
+
123
+ return (
124
+ <View
125
+ onLayout={(e) => setWidth(Math.floor(e.nativeEvent.layout.width))}
126
+ style={[styles.root, disabled && styles.disabled, style]}
127
+ >
128
+ <Animated.View style={{ transform: [{ translateX: shake }] }}>
129
+ <Pressable
130
+ accessibilityRole="button"
131
+ accessibilityLabel={a11yLabel}
132
+ accessibilityHint="Activates the code field"
133
+ accessibilityState={{ disabled: !!disabled }}
134
+ disabled={disabled}
135
+ onPress={() => input.current?.focus()}
136
+ style={styles.row}
137
+ >
138
+ {Array.from({ length }, (_, i) => (
139
+ <React.Fragment key={i}>
140
+ {sep && i === sep ? (
141
+ <View style={[styles.sep, { width: SEP - GAP, height: cellH }]}>
142
+ <View style={[styles.dash, { backgroundColor: colors.mutedForeground }]} />
143
+ </View>
144
+ ) : null}
145
+ <Cell
146
+ index={i}
147
+ char={code[i]}
148
+ secure={secure}
149
+ active={focused && i === activeIndex}
150
+ tone={tone}
151
+ state={state}
152
+ variant={variant}
153
+ w={cell}
154
+ h={cellH}
155
+ />
156
+ </React.Fragment>
157
+ ))}
158
+ </Pressable>
159
+ </Animated.View>
160
+ <TextInput
161
+ ref={input}
162
+ value={code}
163
+ onChangeText={change}
164
+ onFocus={() => setFocused(true)}
165
+ onBlur={() => setFocused(false)}
166
+ editable={!disabled}
167
+ keyboardType="number-pad"
168
+ textContentType="oneTimeCode"
169
+ autoComplete="one-time-code"
170
+ autoFocus={autoFocus}
171
+ caretHidden
172
+ accessibilityLabel={a11yLabel}
173
+ accessibilityHint={
174
+ error ? `Invalid code. Enter your ${length}-digit verification code` : `Enter your ${length}-digit verification code`
175
+ }
176
+ importantForAccessibility="yes"
177
+ style={[styles.hidden, webNoOutline]}
178
+ />
179
+ {message ? <Message text={message} kind={state} /> : null}
180
+ </View>
181
+ );
182
+ }
183
+
184
+ function Cell({
185
+ index,
186
+ char,
187
+ secure,
188
+ active,
189
+ tone,
190
+ state,
191
+ variant,
192
+ w,
193
+ h,
194
+ }: {
195
+ index: number;
196
+ char?: string;
197
+ secure?: boolean;
198
+ active: boolean;
199
+ tone: string;
200
+ state: CellState;
201
+ variant: OtpInputVariant;
202
+ w: number;
203
+ h: number;
204
+ }) {
205
+ const { colors, radius } = useKit();
206
+ const filled = !!char;
207
+ // Native driver: digit pop, caret blink, halo, success pulse.
208
+ const pop = useRef(new Animated.Value(filled ? 1 : 0)).current;
209
+ const caret = useRef(new Animated.Value(0)).current;
210
+ const halo = useRef(new Animated.Value(0)).current;
211
+ const pulse = useRef(new Animated.Value(0)).current;
212
+ // JS driver: border / fill colours.
213
+ const ring = useRef(new Animated.Value(0)).current;
214
+
215
+ useEffect(() => {
216
+ if (char) {
217
+ pop.setValue(0.3);
218
+ Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
219
+ } else {
220
+ Animated.timing(pop, { toValue: 0, duration: 110, useNativeDriver: true }).start();
221
+ }
222
+ }, [char, pop]);
223
+
224
+ useEffect(() => {
225
+ const to = active ? 1 : 0;
226
+ Animated.spring(halo, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
227
+ Animated.timing(ring, { toValue: to, duration: motion.fast, useNativeDriver: false }).start();
228
+ if (!active) return;
229
+ caret.setValue(1);
230
+ const blink = Animated.loop(
231
+ Animated.sequence([
232
+ Animated.delay(450),
233
+ Animated.timing(caret, { toValue: 0, duration: 140, useNativeDriver: true }),
234
+ Animated.delay(300),
235
+ Animated.timing(caret, { toValue: 1, duration: 140, useNativeDriver: true }),
236
+ ])
237
+ );
238
+ blink.start();
239
+ return () => blink.stop();
240
+ }, [active, halo, ring, caret]);
241
+
242
+ useEffect(() => {
243
+ if (state !== 'success') return;
244
+ pulse.setValue(0);
245
+ Animated.sequence([
246
+ Animated.delay(index * 55),
247
+ Animated.timing(pulse, { toValue: 1, duration: 140, easing: Easing.out(Easing.quad), useNativeDriver: true }),
248
+ Animated.spring(pulse, { toValue: 0, useNativeDriver: true, ...motion.bouncy }),
249
+ ]).start();
250
+ }, [state, index, pulse]);
251
+
252
+ const underline = variant === 'underline';
253
+ const pill = variant === 'pill';
254
+ // Box cells follow KitProvider radius; pill stays round; underline has no corners.
255
+ const r = pill ? h / 2 : underline ? 0 : radius.md;
256
+ const rest = pill ? 'transparent' : filled ? alpha(colors.foreground, 0.32) : colors.input;
257
+ const borderColor =
258
+ state !== 'idle' ? tone : ring.interpolate({ inputRange: [0, 1], outputRange: [rest, tone] });
259
+ const fill =
260
+ state === 'success'
261
+ ? alpha(colors.success, 0.12)
262
+ : state === 'error'
263
+ ? alpha(colors.danger, 0.08)
264
+ : underline
265
+ ? 'transparent'
266
+ : pill
267
+ ? colors.muted
268
+ : colors.card;
269
+ const fg = state === 'idle' ? colors.foreground : tone;
270
+ const font = Math.round(w * 0.46);
271
+ const dot = Math.max(8, Math.round(w * 0.22));
272
+
273
+ return (
274
+ <Animated.View
275
+ accessibilityElementsHidden
276
+ importantForAccessibility="no-hide-descendants"
277
+ style={{
278
+ width: w,
279
+ height: h,
280
+ transform: [{ scale: pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.1] }) }],
281
+ }}
282
+ >
283
+ <Animated.View
284
+ pointerEvents="none"
285
+ style={[
286
+ styles.halo,
287
+ {
288
+ borderRadius: underline ? 10 : r + 4,
289
+ backgroundColor: alpha(tone, 0.18),
290
+ opacity: halo,
291
+ transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.86, 1] }) }],
292
+ },
293
+ ]}
294
+ />
295
+ <Animated.View
296
+ style={[
297
+ styles.cell,
298
+ underline
299
+ ? { borderBottomWidth: 2.5, borderColor, backgroundColor: fill }
300
+ : { borderWidth: 1.5, borderRadius: r, borderColor, backgroundColor: fill },
301
+ ]}
302
+ >
303
+ {filled ? (
304
+ <Animated.View style={{ opacity: pop, transform: [{ scale: pop }] }}>
305
+ {secure ? (
306
+ <View style={{ width: dot, height: dot, borderRadius: dot / 2, backgroundColor: fg }} />
307
+ ) : (
308
+ <Text style={[styles.char, { color: fg, fontSize: font, lineHeight: Math.round(font * 1.2) }]}>{char}</Text>
309
+ )}
310
+ </Animated.View>
311
+ ) : active ? (
312
+ <Animated.View style={[styles.caret, { height: Math.round(h * 0.42), backgroundColor: tone, opacity: caret }]} />
313
+ ) : null}
314
+ </Animated.View>
315
+ </Animated.View>
316
+ );
317
+ }
318
+
319
+ /** Helper / error / success line that fades and slides in whenever its text changes. */
320
+ function Message({ text, kind }: { text: string; kind: CellState }) {
321
+ const { colors } = useKit();
322
+ const enter = useRef(new Animated.Value(0)).current;
323
+ const draw = useRef(new Animated.Value(0)).current;
324
+
325
+ useEffect(() => {
326
+ enter.setValue(0);
327
+ Animated.timing(enter, { toValue: 1, duration: motion.normal, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
328
+ draw.setValue(0);
329
+ if (kind === 'success') {
330
+ Animated.timing(draw, { toValue: 1, duration: 360, delay: 120, easing: Easing.out(Easing.cubic), useNativeDriver: false }).start();
331
+ }
332
+ }, [text, kind, enter, draw]);
333
+
334
+ const color = kind === 'error' ? colors.danger : kind === 'success' ? colors.success : colors.mutedForeground;
335
+ return (
336
+ <Animated.View
337
+ accessibilityLiveRegion="polite"
338
+ style={[
339
+ styles.message,
340
+ { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }] },
341
+ ]}
342
+ >
343
+ {kind === 'error' ? (
344
+ <Svg width={14} height={14} viewBox="0 0 24 24" fill="none">
345
+ <Circle cx={12} cy={12} r={9} stroke={color} strokeWidth={1.8} />
346
+ <Path d="M12 7.6v5.2M12 16.3v.1" stroke={color} strokeWidth={1.8} strokeLinecap="round" />
347
+ </Svg>
348
+ ) : kind === 'success' ? (
349
+ <Svg width={14} height={14} viewBox="0 0 24 24" fill="none">
350
+ <Circle cx={12} cy={12} r={9} stroke={color} strokeWidth={1.8} />
351
+ <AnimatedPath
352
+ d="M8 12.3l2.7 2.7L16 9.6"
353
+ stroke={color}
354
+ strokeWidth={1.8}
355
+ strokeLinecap="round"
356
+ strokeLinejoin="round"
357
+ strokeDasharray={CHECK_LEN}
358
+ strokeDashoffset={draw.interpolate({ inputRange: [0, 1], outputRange: [CHECK_LEN, 0] }) as unknown as number}
359
+ />
360
+ </Svg>
361
+ ) : null}
362
+ <Text style={[styles.messageText, { color }]}>{text}</Text>
363
+ </Animated.View>
364
+ );
365
+ }
366
+
367
+ const styles = StyleSheet.create({
368
+ root: { alignSelf: 'stretch' },
369
+ disabled: { opacity: 0.5 },
370
+ row: { flexDirection: 'row', gap: GAP, paddingHorizontal: 4, paddingVertical: 4, justifyContent: 'center', alignItems: 'center' },
371
+ halo: { position: 'absolute', top: -4, left: -4, right: -4, bottom: -4 },
372
+ cell: { flex: 1, alignItems: 'center', justifyContent: 'center' },
373
+ char: { fontWeight: '400', fontVariant: ['tabular-nums'], textAlign: 'center' },
374
+ caret: { width: 2, borderRadius: 1 },
375
+ sep: { alignItems: 'center', justifyContent: 'center' },
376
+ dash: { width: 10, height: 2, borderRadius: 1, opacity: 0.6 },
377
+ message: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: 6 },
378
+ messageText: { fontSize: 12.5, lineHeight: 17, fontWeight: '400' },
379
+ hidden: { position: 'absolute', width: 1, height: 1, opacity: 0 },
380
+ });
@@ -0,0 +1,219 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { Platform, useColorScheme, type TextStyle } from 'react-native';
3
+
4
+ /**
5
+ * ExpoKit design tokens. Every component reads them through useKit(), so one
6
+ * <KitProvider> (optional) re-skins the whole kit: light / dark, accent colour, radius.
7
+ * Without a provider, components follow the system colour scheme.
8
+ */
9
+
10
+ /**
11
+ * Bumped whenever components start using a new kit API (2: radius.scale / radius.capsule).
12
+ * The CLI won't keep an edited kit.tsx older than the component being added needs.
13
+ */
14
+ export const KIT_VERSION = 2;
15
+
16
+ export type ColorScheme = 'light' | 'dark';
17
+
18
+ export interface Colors {
19
+ background: string;
20
+ foreground: string;
21
+ /** Cards, sheets, inputs. */
22
+ card: string;
23
+ cardForeground: string;
24
+ /** Raised surfaces: popovers, toasts. */
25
+ popover: string;
26
+ muted: string;
27
+ mutedForeground: string;
28
+ border: string;
29
+ input: string;
30
+ primary: string;
31
+ primaryForeground: string;
32
+ secondary: string;
33
+ secondaryForeground: string;
34
+ accent: string;
35
+ accentForeground: string;
36
+ success: string;
37
+ warning: string;
38
+ danger: string;
39
+ dangerForeground: string;
40
+ ring: string;
41
+ overlay: string;
42
+ }
43
+
44
+ export interface Theme {
45
+ dark: boolean;
46
+ colors: Colors;
47
+ /** Corner radius scale. `radius.full` makes pills. */
48
+ radius: {
49
+ sm: number;
50
+ md: number;
51
+ lg: number;
52
+ xl: number;
53
+ full: number;
54
+ /** A corner designed at the default radius (12), scaled to the current one: `radius.scale(16)` is 16 at 12, 0 at 0. */
55
+ scale: (n: number) => number;
56
+ /** Corner for a bar or chip of height `h`: a pill at radius ≥ 12, softer below, square at 0. */
57
+ capsule: (h: number) => number;
58
+ };
59
+ /** 4pt spacing scale: space(4) === 16. */
60
+ space: (n: number) => number;
61
+ font: { family?: string; mono: string };
62
+ text: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', TextStyle>;
63
+ }
64
+
65
+ const light: Colors = {
66
+ background: '#ffffff',
67
+ foreground: '#09090b',
68
+ card: '#ffffff',
69
+ cardForeground: '#09090b',
70
+ popover: '#ffffff',
71
+ muted: '#f4f4f5',
72
+ mutedForeground: '#71717a',
73
+ border: '#e4e4e7',
74
+ input: '#d4d4d8',
75
+ primary: '#18181b',
76
+ primaryForeground: '#fafafa',
77
+ secondary: '#f4f4f5',
78
+ secondaryForeground: '#18181b',
79
+ accent: '#18181b',
80
+ accentForeground: '#ffffff',
81
+ success: '#16a34a',
82
+ warning: '#f59e0b',
83
+ danger: '#e5484d',
84
+ dangerForeground: '#ffffff',
85
+ ring: '#18181b',
86
+ overlay: 'rgba(0,0,0,0.4)',
87
+ };
88
+
89
+ const dark: Colors = {
90
+ background: '#09090b',
91
+ foreground: '#fafafa',
92
+ card: '#111113',
93
+ cardForeground: '#fafafa',
94
+ popover: '#18181b',
95
+ muted: '#1c1c20',
96
+ mutedForeground: '#a1a1aa',
97
+ border: '#27272a',
98
+ input: '#34343a',
99
+ primary: '#fafafa',
100
+ primaryForeground: '#18181b',
101
+ secondary: '#1f1f23',
102
+ secondaryForeground: '#fafafa',
103
+ accent: '#fafafa',
104
+ accentForeground: '#09090b',
105
+ success: '#3dd68c',
106
+ warning: '#ffb224',
107
+ danger: '#ff6369',
108
+ dangerForeground: '#ffffff',
109
+ ring: '#d4d4d8',
110
+ overlay: 'rgba(0,0,0,0.6)',
111
+ };
112
+
113
+ export const palettes = { light, dark };
114
+
115
+ function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
116
+ const base = scheme === 'dark' ? dark : light;
117
+ const accent = overrides?.accent;
118
+ const colors: Colors = {
119
+ ...base,
120
+ ...(accent ? { accent, ring: accent, accentForeground: readableOn(accent) } : null),
121
+ ...overrides?.colors?.[scheme],
122
+ };
123
+ const r = overrides?.radius ?? 12;
124
+ return {
125
+ dark: scheme === 'dark',
126
+ colors,
127
+ radius: {
128
+ sm: Math.round(r * 0.5),
129
+ md: r,
130
+ lg: Math.round(r * 1.35),
131
+ xl: Math.round(r * 1.8),
132
+ full: 999,
133
+ scale: (n) => Math.round((n * r) / 12),
134
+ capsule: (h) => Math.round((h / 2) * Math.min(1, r / 12)),
135
+ },
136
+ space: (n) => n * 4,
137
+ font: {
138
+ family: overrides?.fontFamily,
139
+ mono: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'ui-monospace, SFMono-Regular, Menlo, monospace' })!,
140
+ },
141
+ text: {
142
+ xs: { fontSize: 12, lineHeight: 16 },
143
+ sm: { fontSize: 14, lineHeight: 20 },
144
+ md: { fontSize: 16, lineHeight: 22 },
145
+ lg: { fontSize: 18, lineHeight: 24, fontWeight: '400' },
146
+ xl: { fontSize: 22, lineHeight: 28, fontWeight: '400', letterSpacing: -0.3 },
147
+ xxl: { fontSize: 30, lineHeight: 36, fontWeight: '400', letterSpacing: -0.8 },
148
+ },
149
+ };
150
+ }
151
+
152
+ export interface ThemeOverrides {
153
+ /** Brand colour used for focus rings, selected states and accent buttons. */
154
+ accent?: string;
155
+ /** Base corner radius in points (default 12). */
156
+ radius?: number;
157
+ fontFamily?: string;
158
+ /** Override any token per scheme. */
159
+ colors?: Partial<Record<ColorScheme, Partial<Colors>>>;
160
+ }
161
+
162
+ const KitContext = createContext<{ scheme?: ColorScheme; overrides?: ThemeOverrides } | null>(null);
163
+
164
+ export function KitProvider({
165
+ scheme,
166
+ children,
167
+ ...overrides
168
+ }: ThemeOverrides & { scheme?: ColorScheme | 'system'; children: React.ReactNode }) {
169
+ const value = useMemo(
170
+ () => ({ scheme: scheme === 'system' ? undefined : scheme, overrides }),
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ [scheme, overrides.accent, overrides.radius, overrides.fontFamily, overrides.colors]
173
+ );
174
+ return <KitContext.Provider value={value}>{children}</KitContext.Provider>;
175
+ }
176
+
177
+ export function useKit(): Theme {
178
+ const ctx = useContext(KitContext);
179
+ const system = useColorScheme();
180
+ const scheme: ColorScheme = ctx?.scheme ?? (system === 'dark' ? 'dark' : 'light');
181
+ return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
+ }
183
+
184
+ /** Shared motion presets so the whole kit moves the same way. */
185
+ export const motion = {
186
+ /** Toggles, selection indicators. */
187
+ snappy: { damping: 18, stiffness: 260, mass: 0.8 },
188
+ /** Press feedback, pops. */
189
+ bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
190
+ /** Sheets, layout changes. */
191
+ gentle: { damping: 22, stiffness: 160, mass: 1 },
192
+ fast: 160,
193
+ normal: 240,
194
+ };
195
+
196
+ /** Adds alpha to a #rrggbb colour: alpha('#6366f1', 0.2). */
197
+ export function alpha(hex: string, a: number) {
198
+ if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex;
199
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
200
+ return full + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0');
201
+ }
202
+
203
+ /** Black or white, whichever reads better on `hex`. */
204
+ export function readableOn(hex: string) {
205
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
206
+ const n = parseInt(full.slice(1, 7), 16);
207
+ if (Number.isNaN(n)) return '#ffffff';
208
+ const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
209
+ return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#09090b' : '#ffffff';
210
+ }
211
+
212
+ /**
213
+ * True for children that must be wrapped in <Text>: strings, numbers and arrays of them —
214
+ * `{count} new` is the array [3, ' new'], and raw text outside <Text> crashes on native.
215
+ */
216
+ export function isTextNode(node: React.ReactNode): node is string | number | (string | number)[] {
217
+ if (typeof node === 'string' || typeof node === 'number') return true;
218
+ return Array.isArray(node) && node.length > 0 && node.every((n) => typeof n === 'string' || typeof n === 'number');
219
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "otp-input",
3
+ "type": "component",
4
+ "title": "OTP Input",
5
+ "description": "One-time-code cells that size themselves to fit any width. The active cell glows with a soft focus halo and a blinking caret, digits pop in on a spring, a pasted code fills every cell, errors shake the row and success pulses across the cells with a drawn check.",
6
+ "tier": "free",
7
+ "dependencies": [
8
+ "react-native-svg"
9
+ ],
10
+ "registryDependencies": [
11
+ "kit"
12
+ ],
13
+ "sharedFiles": [
14
+ "lib/kit.tsx"
15
+ ],
16
+ "filesDir": "otp-input/files"
17
+ }