@expokit/cli 0.0.0-stage → 0.3.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 (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/bin/expo-kit.js +1266 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +345 -0
  6. package/package.json +49 -4
  7. package/registry/_docs/accordion.json +115 -0
  8. package/registry/_docs/action-sheet.json +138 -0
  9. package/registry/_docs/alert.json +155 -0
  10. package/registry/_docs/avatar.json +227 -0
  11. package/registry/_docs/badge.json +148 -0
  12. package/registry/_docs/bottom-nav.json +143 -0
  13. package/registry/_docs/bottom-sheet.json +186 -0
  14. package/registry/_docs/bullet.json +27 -0
  15. package/registry/_docs/button-3d.json +138 -0
  16. package/registry/_docs/button.json +131 -0
  17. package/registry/_docs/calendar.json +198 -0
  18. package/registry/_docs/card-stack.json +124 -0
  19. package/registry/_docs/card.json +131 -0
  20. package/registry/_docs/carousel.json +122 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +109 -0
  23. package/registry/_docs/checkout.json +105 -0
  24. package/registry/_docs/chip-group.json +132 -0
  25. package/registry/_docs/color-swatch.json +82 -0
  26. package/registry/_docs/confetti.json +102 -0
  27. package/registry/_docs/coupon.json +192 -0
  28. package/registry/_docs/cube-pager.json +100 -0
  29. package/registry/_docs/dashboard.json +123 -0
  30. package/registry/_docs/date-picker.json +269 -0
  31. package/registry/_docs/dialog.json +186 -0
  32. package/registry/_docs/drawer.json +199 -0
  33. package/registry/_docs/fab.json +144 -0
  34. package/registry/_docs/flip-card.json +127 -0
  35. package/registry/_docs/floating-input.json +211 -0
  36. package/registry/_docs/gift-card.json +124 -0
  37. package/registry/_docs/glass-button.json +142 -0
  38. package/registry/_docs/glass-nav-bar.json +102 -0
  39. package/registry/_docs/glass-search-bar.json +114 -0
  40. package/registry/_docs/glass-segmented.json +94 -0
  41. package/registry/_docs/glass-sheet.json +136 -0
  42. package/registry/_docs/glass-tab-bar.json +100 -0
  43. package/registry/_docs/glass.json +94 -0
  44. package/registry/_docs/inbox.json +75 -0
  45. package/registry/_docs/isometric-bar.json +75 -0
  46. package/registry/_docs/kit.json +14 -0
  47. package/registry/_docs/kpi-card.json +14 -0
  48. package/registry/_docs/like-button.json +92 -0
  49. package/registry/_docs/liquidglass-tab.json +30 -0
  50. package/registry/_docs/list-item.json +216 -0
  51. package/registry/_docs/loyalty-card.json +124 -0
  52. package/registry/_docs/morph-button.json +128 -0
  53. package/registry/_docs/number-ticker.json +112 -0
  54. package/registry/_docs/offers.json +81 -0
  55. package/registry/_docs/onboarding.json +69 -0
  56. package/registry/_docs/otp-input.json +158 -0
  57. package/registry/_docs/pagination-dots.json +108 -0
  58. package/registry/_docs/parallax-layers.json +104 -0
  59. package/registry/_docs/parallax-scroll.json +210 -0
  60. package/registry/_docs/password-input.json +112 -0
  61. package/registry/_docs/payment-card.json +169 -0
  62. package/registry/_docs/pixel-bar.json +69 -0
  63. package/registry/_docs/polar-area.json +81 -0
  64. package/registry/_docs/profile.json +87 -0
  65. package/registry/_docs/progress-steps.json +116 -0
  66. package/registry/_docs/progress.json +152 -0
  67. package/registry/_docs/radio-group.json +112 -0
  68. package/registry/_docs/rating.json +114 -0
  69. package/registry/_docs/ring-carousel.json +122 -0
  70. package/registry/_docs/scratch-card.json +110 -0
  71. package/registry/_docs/search-bar.json +160 -0
  72. package/registry/_docs/search-results.json +123 -0
  73. package/registry/_docs/segmented-control.json +123 -0
  74. package/registry/_docs/select.json +277 -0
  75. package/registry/_docs/settings.json +75 -0
  76. package/registry/_docs/shimmer-button.json +98 -0
  77. package/registry/_docs/sign-in.json +201 -0
  78. package/registry/_docs/skeleton.json +162 -0
  79. package/registry/_docs/slide-to-confirm.json +108 -0
  80. package/registry/_docs/slider.json +177 -0
  81. package/registry/_docs/spin-wheel.json +138 -0
  82. package/registry/_docs/spinner.json +121 -0
  83. package/registry/_docs/stateful-button.json +184 -0
  84. package/registry/_docs/stepper.json +184 -0
  85. package/registry/_docs/swipe-deck.json +118 -0
  86. package/registry/_docs/swipeable-row.json +95 -0
  87. package/registry/_docs/switch.json +124 -0
  88. package/registry/_docs/tabs.json +129 -0
  89. package/registry/_docs/tag-input.json +148 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +191 -0
  92. package/registry/_docs/ticket-card.json +194 -0
  93. package/registry/_docs/ticket-shape.json +14 -0
  94. package/registry/_docs/tilt-card.json +157 -0
  95. package/registry/_docs/time-picker.json +269 -0
  96. package/registry/_docs/timeline-chart.json +33 -0
  97. package/registry/_docs/timeline.json +172 -0
  98. package/registry/_docs/toast.json +185 -0
  99. package/registry/_docs/treemap.json +33 -0
  100. package/registry/_docs/waterfall.json +39 -0
  101. package/registry/_docs/wheel-list.json +122 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  103. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  104. package/registry/accordion.json +17 -0
  105. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  106. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  107. package/registry/action-sheet.json +18 -0
  108. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  109. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  110. package/registry/alert/files/src/lib/kit.tsx +219 -0
  111. package/registry/alert.json +20 -0
  112. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  113. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  114. package/registry/avatar.json +18 -0
  115. package/registry/badge/files/src/components/ui/badge.tsx +626 -0
  116. package/registry/badge/files/src/lib/kit.tsx +219 -0
  117. package/registry/badge.json +17 -0
  118. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  119. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-nav.json +18 -0
  121. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  122. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  123. package/registry/bottom-sheet.json +17 -0
  124. package/registry/bullet.json +14 -0
  125. package/registry/button/files/src/components/ui/button.tsx +274 -0
  126. package/registry/button/files/src/lib/kit.tsx +219 -0
  127. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  128. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  129. package/registry/button-3d.json +18 -0
  130. package/registry/button.json +18 -0
  131. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  132. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  133. package/registry/calendar.json +17 -0
  134. package/registry/card/files/src/components/ui/card.tsx +300 -0
  135. package/registry/card/files/src/lib/kit.tsx +219 -0
  136. package/registry/card-stack.json +15 -0
  137. package/registry/card.json +17 -0
  138. package/registry/carousel.json +15 -0
  139. package/registry/chat.json +18 -0
  140. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  141. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  142. package/registry/checkbox.json +17 -0
  143. package/registry/checkout.json +26 -0
  144. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  145. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  146. package/registry/chip-group.json +17 -0
  147. package/registry/color-swatch.json +17 -0
  148. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  149. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  150. package/registry/confetti.json +15 -0
  151. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  152. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  153. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  154. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  155. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  156. package/registry/coupon.json +23 -0
  157. package/registry/cube-pager.json +15 -0
  158. package/registry/dashboard.json +32 -0
  159. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  160. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  161. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  162. package/registry/date-picker.json +19 -0
  163. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  164. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  165. package/registry/dialog.json +18 -0
  166. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  167. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  168. package/registry/drawer.json +18 -0
  169. package/registry/fab.json +18 -0
  170. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  171. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  172. package/registry/flip-card.json +15 -0
  173. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  174. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  175. package/registry/floating-input.json +17 -0
  176. package/registry/gift-card.json +21 -0
  177. package/registry/glass-button.json +22 -0
  178. package/registry/glass-nav-bar.json +21 -0
  179. package/registry/glass-search-bar.json +21 -0
  180. package/registry/glass-segmented.json +22 -0
  181. package/registry/glass-sheet.json +22 -0
  182. package/registry/glass-tab-bar.json +22 -0
  183. package/registry/glass.json +19 -0
  184. package/registry/inbox.json +30 -0
  185. package/registry/isometric-bar.json +14 -0
  186. package/registry/kit/files/src/lib/kit.tsx +219 -0
  187. package/registry/kit.json +11 -0
  188. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  189. package/registry/kpi-card.json +14 -0
  190. package/registry/like-button.json +18 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  192. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  193. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  194. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  195. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  196. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  197. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  198. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  199. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  200. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  201. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  202. package/registry/liquidglass-tab.json +23 -0
  203. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  204. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  205. package/registry/list-item.json +17 -0
  206. package/registry/loyalty-card.json +23 -0
  207. package/registry/morph-button.json +18 -0
  208. package/registry/number-ticker.json +15 -0
  209. package/registry/offers.json +24 -0
  210. package/registry/onboarding.json +17 -0
  211. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  212. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  213. package/registry/otp-input.json +17 -0
  214. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  215. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  216. package/registry/pagination-dots.json +15 -0
  217. package/registry/parallax-layers.json +15 -0
  218. package/registry/parallax-scroll.json +17 -0
  219. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  220. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  221. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  222. package/registry/password-input.json +19 -0
  223. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  224. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  225. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  226. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  227. package/registry/payment-card.json +21 -0
  228. package/registry/pixel-bar.json +14 -0
  229. package/registry/polar-area.json +14 -0
  230. package/registry/profile.json +17 -0
  231. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  232. package/registry/progress/files/src/lib/kit.tsx +219 -0
  233. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  234. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  235. package/registry/progress-steps.json +18 -0
  236. package/registry/progress.json +17 -0
  237. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  238. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  239. package/registry/radio-group.json +18 -0
  240. package/registry/rating.json +18 -0
  241. package/registry/ring-carousel.json +15 -0
  242. package/registry/scratch-card.json +18 -0
  243. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  244. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  245. package/registry/search-bar.json +17 -0
  246. package/registry/search-results.json +30 -0
  247. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  248. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  249. package/registry/segmented-control.json +18 -0
  250. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  251. package/registry/select/files/src/lib/kit.tsx +219 -0
  252. package/registry/select.json +17 -0
  253. package/registry/settings.json +26 -0
  254. package/registry/shimmer-button.json +17 -0
  255. package/registry/sign-in.json +26 -0
  256. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  257. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  258. package/registry/skeleton.json +17 -0
  259. package/registry/slide-to-confirm.json +18 -0
  260. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  261. package/registry/slider/files/src/lib/kit.tsx +219 -0
  262. package/registry/slider.json +17 -0
  263. package/registry/spin-wheel.json +18 -0
  264. package/registry/spinner/files/src/components/ui/spinner.tsx +748 -0
  265. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  266. package/registry/spinner.json +17 -0
  267. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  268. package/registry/stateful-button/files/src/lib/kit.tsx +219 -0
  269. package/registry/stateful-button.json +18 -0
  270. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  271. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  272. package/registry/stepper.json +17 -0
  273. package/registry/swipe-deck.json +15 -0
  274. package/registry/swipeable-row.json +15 -0
  275. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  276. package/registry/switch/files/src/lib/kit.tsx +219 -0
  277. package/registry/switch.json +18 -0
  278. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  279. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  280. package/registry/tabs.json +17 -0
  281. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  282. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  283. package/registry/tag-input.json +17 -0
  284. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  285. package/registry/text-animation/files/src/lib/kit.tsx +219 -0
  286. package/registry/text-animation.json +15 -0
  287. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  288. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  289. package/registry/textarea.json +17 -0
  290. package/registry/ticket-card.json +24 -0
  291. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  292. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  293. package/registry/ticket-shape.json +17 -0
  294. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  295. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  296. package/registry/tilt-card.json +17 -0
  297. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  298. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  299. package/registry/time-picker.json +17 -0
  300. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  301. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  302. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  303. package/registry/timeline-chart.json +14 -0
  304. package/registry/timeline.json +20 -0
  305. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  306. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  307. package/registry/toast/files/src/lib/kit.tsx +219 -0
  308. package/registry/toast.json +20 -0
  309. package/registry/treemap.json +14 -0
  310. package/registry/waterfall.json +14 -0
  311. package/registry/wheel-list.json +17 -0
  312. package/schema/components.schema.json +13 -0
@@ -0,0 +1,559 @@
1
+ import * as Haptics from 'expo-haptics';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ import {
4
+ AccessibilityInfo,
5
+ Animated,
6
+ Easing,
7
+ Pressable,
8
+ StyleSheet,
9
+ Text,
10
+ View,
11
+ type LayoutChangeEvent,
12
+ type StyleProp,
13
+ type ViewStyle,
14
+ } from 'react-native';
15
+ import Svg, { Path, Rect } from 'react-native-svg';
16
+
17
+ import { alpha, motion, readableOn, useKit } from '@/lib/kit';
18
+
19
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
20
+
21
+ export type ProgressStepsVariant = 'horizontal' | 'vertical' | 'bar';
22
+
23
+ export interface ProgressStep {
24
+ label: string;
25
+ /** Muted line under the label (two lines max in the horizontal variant). */
26
+ description?: string;
27
+ }
28
+
29
+ export interface ProgressStepsProps {
30
+ /** Step labels, or objects with a description. */
31
+ steps: (string | ProgressStep)[];
32
+ /** Index of the active step. Steps before it are complete; `steps.length` marks everything done. */
33
+ current: number;
34
+ /** Index of a step that failed (drawn red with "!"). */
35
+ errorStep?: number;
36
+ /** 'horizontal' nodes with labels · 'vertical' list with descriptions · 'bar' compact "Step 2 of 4" with a segmented bar. */
37
+ variant?: ProgressStepsVariant;
38
+ /** Lets people tap a step, e.g. to go back to a completed one. */
39
+ onStepPress?: (index: number) => void;
40
+ /** Which steps `onStepPress` makes tappable: 'all' (default) or only 'completed' ones. */
41
+ pressableSteps?: 'all' | 'completed';
42
+ /** Selection tick whenever `current` changes (default true). */
43
+ haptics?: boolean;
44
+ /** Colour of completed steps, connectors and the current ring (default: theme accent). */
45
+ accent?: string;
46
+ style?: StyleProp<ViewStyle>;
47
+ }
48
+
49
+ const NODE = 28;
50
+ const GAP = 6;
51
+ const LINE = 3;
52
+ const H_PAD = 6;
53
+ const CHECK = 'M7 12.4l3.4 3.4L17.2 9';
54
+ const CHECK_LEN = 16;
55
+
56
+ /**
57
+ * Multi-step progress for checkout and onboarding flows. Completed steps fill and draw their
58
+ * check, the current step glows with a pulsing ring, upcoming steps stay hollow. When `current`
59
+ * changes the finished node pops, the connector pours the accent into the next step and the new
60
+ * step lights up, with a selection tick. Horizontal, vertical (with descriptions) and a compact bar.
61
+ */
62
+ export function ProgressSteps({
63
+ steps,
64
+ current,
65
+ errorStep,
66
+ variant = 'horizontal',
67
+ onStepPress,
68
+ pressableSteps = 'all',
69
+ haptics = true,
70
+ accent,
71
+ style,
72
+ }: ProgressStepsProps) {
73
+ const { colors, dark } = useKit();
74
+ const reduce = useReduceMotion();
75
+ const tint = accent ?? colors.accent;
76
+ const items = steps.map((s) => (typeof s === 'string' ? { label: s } : s));
77
+ const n = items.length;
78
+ const target = Math.max(0, Math.min(n, current));
79
+ const track = alpha(colors.foreground, dark ? 0.12 : 0.08);
80
+
81
+ // Same motion on two drivers: native for transforms / opacity, JS for the SVG check stroke.
82
+ const pNative = useRef(new Animated.Value(target)).current;
83
+ const pJs = useRef(new Animated.Value(target)).current;
84
+ const labelIn = useRef(new Animated.Value(1)).current;
85
+ const prev = useRef(target);
86
+ useEffect(() => {
87
+ if (prev.current === target) return;
88
+ prev.current = target;
89
+ if (haptics) Haptics.selectionAsync().catch(() => {});
90
+ if (reduce) {
91
+ const cfg = { toValue: target, duration: 160 };
92
+ Animated.parallel([
93
+ Animated.timing(pNative, { ...cfg, useNativeDriver: true }),
94
+ Animated.timing(pJs, { ...cfg, useNativeDriver: false }),
95
+ ]).start();
96
+ labelIn.setValue(1);
97
+ return;
98
+ }
99
+ const cfg = { toValue: target, ...motion.gentle, overshootClamping: true };
100
+ Animated.parallel([
101
+ Animated.spring(pNative, { ...cfg, useNativeDriver: true }),
102
+ Animated.spring(pJs, { ...cfg, useNativeDriver: false }),
103
+ ]).start();
104
+ labelIn.setValue(0);
105
+ Animated.spring(labelIn, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
106
+ }, [target, haptics, reduce, pNative, pJs, labelIn]);
107
+
108
+ // One shared heartbeat for the current step's ring.
109
+ const pulse = useRef(new Animated.Value(0)).current;
110
+ const pulsing = !reduce && target < n && target !== errorStep && variant !== 'bar';
111
+ useEffect(() => {
112
+ if (!pulsing) {
113
+ pulse.setValue(0);
114
+ return;
115
+ }
116
+ const loop = Animated.loop(
117
+ Animated.sequence([
118
+ Animated.timing(pulse, { toValue: 1, duration: 1500, easing: Easing.out(Easing.quad), useNativeDriver: true }),
119
+ Animated.delay(250),
120
+ ]),
121
+ { resetBeforeIteration: true }
122
+ );
123
+ loop.start();
124
+ return () => loop.stop();
125
+ }, [pulsing, pulse]);
126
+
127
+ const [width, setWidth] = useState(0);
128
+
129
+ const stateOf = (i: number): NodeState =>
130
+ i === errorStep ? 'error' : i < target ? 'done' : i === target ? 'active' : 'upcoming';
131
+
132
+ if (variant === 'bar') {
133
+ const shown = Math.min(target, n - 1);
134
+ const done = target >= n;
135
+ const failed = errorStep === shown;
136
+ return (
137
+ <View
138
+ accessible
139
+ accessibilityRole="progressbar"
140
+ accessibilityLabel={done ? 'All steps complete' : `Step ${shown + 1} of ${n}, ${items[shown]!.label}`}
141
+ accessibilityValue={{ min: 0, max: n, now: target }}
142
+ style={[styles.bar, style]}
143
+ >
144
+ <View style={styles.barHead}>
145
+ <Animated.Text
146
+ numberOfLines={1}
147
+ style={[
148
+ styles.barLabel,
149
+ { color: failed ? colors.danger : colors.foreground },
150
+ {
151
+ opacity: labelIn,
152
+ transform: [{ translateY: labelIn.interpolate({ inputRange: [0, 1], outputRange: [6, 0] }) }],
153
+ },
154
+ ]}
155
+ >
156
+ {done ? 'All done' : items[shown]!.label}
157
+ </Animated.Text>
158
+ <Text style={[styles.barStep, { color: colors.mutedForeground }]}>
159
+ {done ? `${n} of ${n}` : `Step ${shown + 1} of ${n}`}
160
+ </Text>
161
+ </View>
162
+ <View style={styles.barRow}>
163
+ {items.map((_, i) => (
164
+ <Fill
165
+ key={i}
166
+ axis="x"
167
+ progress={pNative}
168
+ range={[i, i + 1]}
169
+ color={i === errorStep ? colors.danger : tint}
170
+ track={i === shown && !done ? alpha(i === errorStep ? colors.danger : tint, dark ? 0.26 : 0.2) : track}
171
+ thickness={6}
172
+ style={styles.flex}
173
+ />
174
+ ))}
175
+ </View>
176
+ </View>
177
+ );
178
+ }
179
+
180
+ const vertical = variant === 'vertical';
181
+ const colW = width / Math.max(1, n);
182
+ const segW = colW - NODE - GAP * 2;
183
+ return (
184
+ <View
185
+ onLayout={vertical ? undefined : (e) => setWidth(Math.round(e.nativeEvent.layout.width))}
186
+ style={[vertical ? styles.vertical : styles.horizontal, style]}
187
+ >
188
+ {/* Horizontal connectors sit between the columns' nodes, measured from the row width. */}
189
+ {!vertical && width > 0 && segW > 0
190
+ ? items.slice(0, -1).map((_, i) => (
191
+ <Fill
192
+ key={i}
193
+ axis="x"
194
+ progress={pNative}
195
+ range={[i + 0.2, i + 0.8]}
196
+ color={tint}
197
+ track={track}
198
+ thickness={LINE}
199
+ style={[styles.hLine, { left: (i + 0.5) * colW + NODE / 2 + GAP, width: segW }]}
200
+ />
201
+ ))
202
+ : null}
203
+ {items.map((step, i) => (
204
+ <Step
205
+ key={i}
206
+ index={i}
207
+ count={n}
208
+ step={step}
209
+ state={stateOf(i)}
210
+ vertical={vertical}
211
+ pNative={pNative}
212
+ pJs={pJs}
213
+ pulse={pulse}
214
+ color={i === errorStep ? colors.danger : tint}
215
+ lineColor={tint}
216
+ track={track}
217
+ reduce={reduce}
218
+ onPress={
219
+ onStepPress && (pressableSteps === 'all' || i < target) && i !== target ? () => onStepPress(i) : undefined
220
+ }
221
+ />
222
+ ))}
223
+ </View>
224
+ );
225
+ }
226
+
227
+ type NodeState = 'done' | 'active' | 'upcoming' | 'error';
228
+
229
+ function Step({
230
+ index: i,
231
+ count: n,
232
+ step,
233
+ state,
234
+ vertical,
235
+ pNative,
236
+ pJs,
237
+ pulse,
238
+ color,
239
+ lineColor,
240
+ track,
241
+ reduce,
242
+ onPress,
243
+ }: {
244
+ index: number;
245
+ count: number;
246
+ step: ProgressStep;
247
+ state: NodeState;
248
+ vertical: boolean;
249
+ pNative: Animated.Value;
250
+ pJs: Animated.Value;
251
+ pulse: Animated.Value;
252
+ color: string;
253
+ lineColor: string;
254
+ track: string;
255
+ reduce: boolean;
256
+ onPress?: () => void;
257
+ }) {
258
+ const { colors } = useKit();
259
+ const press = useRef(new Animated.Value(0)).current;
260
+ const pressTo = (v: number) =>
261
+ Animated.spring(press, { toValue: v, useNativeDriver: true, ...(v ? motion.snappy : motion.bouncy) }).start();
262
+
263
+ const last = i === n - 1;
264
+ const labelColor =
265
+ state === 'error' ? colors.danger : state === 'upcoming' ? colors.mutedForeground : colors.foreground;
266
+ const status =
267
+ state === 'active' ? 'current' : state === 'done' ? 'completed' : state === 'error' ? 'needs attention' : 'not started';
268
+
269
+ const node = (
270
+ <Animated.View style={{ transform: [{ scale: press.interpolate({ inputRange: [0, 1], outputRange: [1, 0.86] }) }] }}>
271
+ <Node index={i} state={state} pNative={pNative} pJs={pJs} pulse={pulse} color={color} reduce={reduce} />
272
+ </Animated.View>
273
+ );
274
+
275
+ const line = (style: StyleProp<ViewStyle>) => (
276
+ <Fill
277
+ axis="y"
278
+ progress={pNative}
279
+ range={[i + 0.2, i + 0.8]}
280
+ color={lineColor}
281
+ track={track}
282
+ thickness={LINE}
283
+ style={style}
284
+ />
285
+ );
286
+
287
+ const a11y = {
288
+ accessibilityRole: onPress ? ('button' as const) : ('text' as const),
289
+ accessibilityLabel: `Step ${i + 1} of ${n}, ${step.label}, ${status}`,
290
+ accessibilityHint: onPress ? 'Goes back to this step' : undefined,
291
+ accessibilityState: { selected: state === 'active' },
292
+ };
293
+ const Wrapper = onPress ? Pressable : View;
294
+ const pressProps = onPress ? { onPress, onPressIn: () => pressTo(1), onPressOut: () => pressTo(0) } : {};
295
+
296
+ if (vertical) {
297
+ return (
298
+ <Wrapper {...a11y} {...pressProps} style={styles.vRow}>
299
+ <View style={styles.vRail}>
300
+ {node}
301
+ {!last ? line(styles.vLine) : null}
302
+ </View>
303
+ <View style={[styles.vBody, !last && styles.vBodyGap]}>
304
+ <Text style={[styles.vLabel, { color: labelColor }]}>{step.label}</Text>
305
+ {step.description ? (
306
+ <Text style={[styles.vDesc, { color: state === 'error' ? colors.danger : colors.mutedForeground }]}>
307
+ {step.description}
308
+ </Text>
309
+ ) : null}
310
+ </View>
311
+ </Wrapper>
312
+ );
313
+ }
314
+
315
+ return (
316
+ <Wrapper {...a11y} {...pressProps} style={styles.hCol}>
317
+ <View style={styles.hTop}>{node}</View>
318
+ <Text numberOfLines={2} style={[styles.hLabel, { color: labelColor }]}>
319
+ {step.label}
320
+ </Text>
321
+ {step.description ? (
322
+ <Text numberOfLines={2} style={[styles.hDesc, { color: colors.mutedForeground }]}>
323
+ {step.description}
324
+ </Text>
325
+ ) : null}
326
+ </Wrapper>
327
+ );
328
+ }
329
+
330
+ function Node({
331
+ index: i,
332
+ state,
333
+ pNative,
334
+ pJs,
335
+ pulse,
336
+ color,
337
+ reduce,
338
+ }: {
339
+ index: number;
340
+ state: NodeState;
341
+ pNative: Animated.Value;
342
+ pJs: Animated.Value;
343
+ pulse: Animated.Value;
344
+ color: string;
345
+ reduce: boolean;
346
+ }) {
347
+ const { colors, radius, dark } = useKit();
348
+ const clamp = 'clamp' as const;
349
+ const round = { borderRadius: radius.capsule(NODE) };
350
+ const activeIn = pNative.interpolate({ inputRange: [i - 0.3, i], outputRange: [0, 1], extrapolate: clamp });
351
+ const doneIn = pNative.interpolate({ inputRange: [i, i + 0.3], outputRange: [0, 1], extrapolate: clamp });
352
+ const draw = pJs.interpolate({ inputRange: [i + 0.1, i + 0.55], outputRange: [CHECK_LEN, 0], extrapolate: clamp });
353
+ const ink = readableOn(color);
354
+
355
+ if (state === 'error') {
356
+ return (
357
+ <View style={styles.nodeBox}>
358
+ <View style={[styles.halo, { borderRadius: radius.capsule(NODE + 8), backgroundColor: alpha(color, 0.16) }]} />
359
+ <Disc color={color} corner={radius.capsule(NODE)}>
360
+ <Path d="M12 7v6" stroke={ink} strokeWidth={2.6} strokeLinecap="round" />
361
+ <Rect x={10.6} y={15.6} width={2.8} height={2.8} rx={1.4} fill={ink} />
362
+ </Disc>
363
+ </View>
364
+ );
365
+ }
366
+
367
+ return (
368
+ <View style={styles.nodeBox}>
369
+ {/* Upcoming: hollow ring, muted number. */}
370
+ <View
371
+ style={[styles.node, round, { borderWidth: 1.5, borderColor: alpha(colors.foreground, dark ? 0.22 : 0.16) }]}
372
+ >
373
+ <Text style={[styles.num, { color: colors.mutedForeground }]}>{i + 1}</Text>
374
+ </View>
375
+
376
+ {/* Current: soft halo, a ring that breathes outwards, accent ring with a tinted core. */}
377
+ <Animated.View
378
+ style={[
379
+ styles.halo,
380
+ { borderRadius: radius.capsule(NODE + 8), backgroundColor: alpha(color, dark ? 0.2 : 0.14), opacity: activeIn },
381
+ ]}
382
+ />
383
+ {state === 'active' && !reduce ? (
384
+ <Animated.View
385
+ pointerEvents="none"
386
+ style={[
387
+ styles.node,
388
+ styles.layer,
389
+ round,
390
+ {
391
+ borderWidth: 1.5,
392
+ borderColor: color,
393
+ opacity: Animated.multiply(pulse.interpolate({ inputRange: [0, 1], outputRange: [0.5, 0] }), activeIn),
394
+ transform: [{ scale: pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.8] }) }],
395
+ },
396
+ ]}
397
+ />
398
+ ) : null}
399
+ <Animated.View
400
+ style={[
401
+ styles.node,
402
+ styles.layer,
403
+ round,
404
+ {
405
+ borderWidth: 2,
406
+ borderColor: color,
407
+ backgroundColor: colors.background,
408
+ opacity: activeIn,
409
+ transform: [{ scale: activeIn.interpolate({ inputRange: [0, 1], outputRange: [0.8, 1] }) }],
410
+ },
411
+ ]}
412
+ >
413
+ <View style={[StyleSheet.absoluteFill, round, { backgroundColor: alpha(color, 0.12) }]} />
414
+ <Text style={[styles.num, { color }]}>{i + 1}</Text>
415
+ </Animated.View>
416
+
417
+ {/* Done: solid disc that pops in, then the check draws itself. */}
418
+ <Animated.View
419
+ style={[
420
+ styles.layer,
421
+ {
422
+ opacity: doneIn,
423
+ transform: [
424
+ {
425
+ scale: doneIn.interpolate(
426
+ reduce
427
+ ? { inputRange: [0, 1], outputRange: [0.9, 1] }
428
+ : { inputRange: [0, 0.7, 1], outputRange: [0.55, 1.1, 1] }
429
+ ),
430
+ },
431
+ ],
432
+ },
433
+ ]}
434
+ >
435
+ <Disc color={color} corner={radius.capsule(NODE)}>
436
+ <AnimatedPath
437
+ d={CHECK}
438
+ stroke={ink}
439
+ strokeWidth={2.6}
440
+ fill="none"
441
+ strokeLinecap="round"
442
+ strokeLinejoin="round"
443
+ strokeDasharray={`${CHECK_LEN} ${CHECK_LEN}`}
444
+ strokeDashoffset={draw}
445
+ />
446
+ </Disc>
447
+ </Animated.View>
448
+ </View>
449
+ );
450
+ }
451
+
452
+ /** Solid filled node; children draw in a 24×24 box on top. */
453
+ function Disc({ color, corner, children }: { color: string; corner: number; children: React.ReactNode }) {
454
+ const r = (corner / NODE) * 24;
455
+ return (
456
+ <Svg width={NODE} height={NODE} viewBox="0 0 24 24">
457
+ <Rect width={24} height={24} rx={r} fill={color} />
458
+ {children}
459
+ </Svg>
460
+ );
461
+ }
462
+
463
+ /** A track whose solid fill slides in (native driver) as `progress` moves through `range`. */
464
+ function Fill({
465
+ axis,
466
+ progress,
467
+ range,
468
+ color,
469
+ track,
470
+ thickness,
471
+ style,
472
+ }: {
473
+ axis: 'x' | 'y';
474
+ progress: Animated.Value;
475
+ range: [number, number];
476
+ color: string;
477
+ track: string;
478
+ thickness: number;
479
+ style?: StyleProp<ViewStyle>;
480
+ }) {
481
+ const { radius } = useKit();
482
+ const [len, setLen] = useState(0);
483
+ const onLayout = (e: LayoutChangeEvent) => {
484
+ const { width, height } = e.nativeEvent.layout;
485
+ const l = Math.round(axis === 'x' ? width : height);
486
+ if (l !== len) setLen(l);
487
+ };
488
+ const x = axis === 'x';
489
+ const corner = radius.capsule(thickness);
490
+ const shift = progress.interpolate({ inputRange: range, outputRange: [-len, 0], extrapolate: 'clamp' });
491
+ const w = x ? len : thickness;
492
+ const h = x ? thickness : len;
493
+ return (
494
+ <View
495
+ onLayout={onLayout}
496
+ style={[
497
+ x ? { height: thickness } : { width: thickness },
498
+ { borderRadius: corner, backgroundColor: track, overflow: 'hidden' },
499
+ style,
500
+ ]}
501
+ >
502
+ {len ? (
503
+ <Animated.View
504
+ style={{
505
+ width: w,
506
+ height: h,
507
+ borderRadius: corner,
508
+ backgroundColor: color,
509
+ transform: [x ? { translateX: shift } : { translateY: shift }],
510
+ }}
511
+ />
512
+ ) : null}
513
+ </View>
514
+ );
515
+ }
516
+
517
+ function useReduceMotion() {
518
+ const [reduce, setReduce] = useState(false);
519
+ useEffect(() => {
520
+ let alive = true;
521
+ AccessibilityInfo.isReduceMotionEnabled()
522
+ .then((v) => alive && setReduce(v))
523
+ .catch(() => {});
524
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
525
+ return () => {
526
+ alive = false;
527
+ sub.remove();
528
+ };
529
+ }, []);
530
+ return reduce;
531
+ }
532
+
533
+ const styles = StyleSheet.create({
534
+ flex: { flex: 1 },
535
+ horizontal: { flexDirection: 'row', alignItems: 'flex-start', alignSelf: 'stretch' },
536
+ hCol: { flex: 1, alignItems: 'center', minHeight: 44, paddingTop: H_PAD },
537
+ hTop: { marginBottom: 10 },
538
+ hLine: { position: 'absolute', top: H_PAD + NODE / 2 - LINE / 2 },
539
+ hLabel: { fontSize: 12.5, fontWeight: '400', textAlign: 'center', letterSpacing: -0.1, paddingHorizontal: 2 },
540
+ hDesc: { fontSize: 11, lineHeight: 14, textAlign: 'center', marginTop: 2, paddingHorizontal: 2 },
541
+ nodeBox: { width: NODE, height: NODE },
542
+ node: { width: NODE, height: NODE, alignItems: 'center', justifyContent: 'center', overflow: 'hidden' },
543
+ layer: { position: 'absolute', left: 0, top: 0 },
544
+ halo: { position: 'absolute', left: -4, top: -4, width: NODE + 8, height: NODE + 8 },
545
+ num: { fontSize: 12.5, fontWeight: '400', fontVariant: ['tabular-nums'] },
546
+ vertical: { alignSelf: 'stretch' },
547
+ vRow: { flexDirection: 'row', gap: 14, minHeight: 44, paddingLeft: 4 },
548
+ vRail: { alignItems: 'center', width: NODE, paddingTop: 2 },
549
+ vLine: { flex: 1, minHeight: 16, marginTop: GAP + 2, marginBottom: GAP - 4 },
550
+ vBody: { flex: 1, paddingTop: 5 },
551
+ vBodyGap: { paddingBottom: 24 },
552
+ vLabel: { fontSize: 15.5, fontWeight: '400', letterSpacing: -0.25 },
553
+ vDesc: { fontSize: 13.5, lineHeight: 19, marginTop: 3 },
554
+ bar: { alignSelf: 'stretch', gap: 10 },
555
+ barHead: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 },
556
+ barStep: { fontSize: 13, fontWeight: '400', fontVariant: ['tabular-nums'] },
557
+ barLabel: { fontSize: 16, fontWeight: '400', letterSpacing: -0.25, flexShrink: 1 },
558
+ barRow: { flexDirection: 'row', gap: 5 },
559
+ });