@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,883 @@
1
+ import React, { useEffect, useId, useRef, useState } from 'react';
2
+ import {
3
+ AccessibilityInfo,
4
+ Animated,
5
+ Easing,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ type StyleProp,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import Svg, { Circle, Defs, LinearGradient, Path, Polygon, RadialGradient, Rect, Stop } from 'react-native-svg';
13
+
14
+ import { alpha, motion, readableOn, useKit } from '@/lib/kit';
15
+
16
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
17
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
18
+
19
+ export type ProgressVariant = 'linear' | 'circular' | 'steps';
20
+ export type ProgressSize = 'sm' | 'md' | 'lg';
21
+
22
+ export interface ProgressProps {
23
+ variant?: ProgressVariant;
24
+ /** Current value, 0 → `max`. For `steps`, the number of completed steps. Omit for indeterminate. */
25
+ value?: number;
26
+ /** Upper bound (default 100, or `steps` for the steps variant). */
27
+ max?: number;
28
+ /** Sweeping / spinning animation when progress is unknown. Also implied by an undefined `value`. */
29
+ indeterminate?: boolean;
30
+ /** Number of segments for the `steps` variant (default 4). */
31
+ steps?: number;
32
+ size?: ProgressSize;
33
+ /** Fill colour (default: theme accent). Fills are solid; tracks and glows are tints of it. */
34
+ color?: string;
35
+ trackColor?: string;
36
+ /** Caption above a linear / steps bar, or under the ring. */
37
+ label?: string;
38
+ /** Show the percentage (linear: right of the label, circular: in the middle). */
39
+ showValue?: boolean;
40
+ /** Custom text in place of the percentage. */
41
+ formatValue?: (value: number, max: number) => string;
42
+ /** Reaching `max` flashes the fill and turns the value into a drawn check (default true). */
43
+ celebrate?: boolean;
44
+ /** Animated diagonal stripes on the linear fill, for "processing" states. */
45
+ striped?: boolean;
46
+ style?: StyleProp<ViewStyle>;
47
+ }
48
+
49
+ const BAR = { sm: 4, md: 8, lg: 12 };
50
+ const RING = {
51
+ sm: { d: 44, w: 4, font: 11 },
52
+ md: { d: 72, w: 6, font: 16 },
53
+ lg: { d: 112, w: 9, font: 24 },
54
+ };
55
+ const WHITE = '#ffffff';
56
+
57
+ const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
58
+
59
+ function useReduceMotion() {
60
+ const [reduce, setReduce] = useState(false);
61
+ useEffect(() => {
62
+ let alive = true;
63
+ AccessibilityInfo.isReduceMotionEnabled()
64
+ .then((r) => alive && setReduce(r))
65
+ .catch(() => {});
66
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
67
+ return () => {
68
+ alive = false;
69
+ sub.remove();
70
+ };
71
+ }, []);
72
+ return reduce;
73
+ }
74
+
75
+ /** True only on the render where `on` flips from false to true. */
76
+ function useBecameTrue(on: boolean) {
77
+ const [prev, setPrev] = useState(on);
78
+ const [fired, setFired] = useState(0);
79
+ if (on !== prev) {
80
+ setPrev(on);
81
+ if (on) setFired((n) => n + 1);
82
+ }
83
+ return fired;
84
+ }
85
+
86
+ /**
87
+ * Progress in three shapes — a solid bar with a soft glowing head (determinate, indeterminate or
88
+ * striped), an SVG ring whose percentage counts up and morphs into a check, and segmented steps that pop
89
+ * as they fill. Reaching `max` plays a short success moment. Transforms run on the native driver.
90
+ */
91
+ export function Progress({
92
+ variant = 'linear',
93
+ value,
94
+ max,
95
+ indeterminate,
96
+ steps = 4,
97
+ size = 'md',
98
+ color,
99
+ trackColor,
100
+ label,
101
+ showValue,
102
+ formatValue,
103
+ celebrate = true,
104
+ striped = false,
105
+ style,
106
+ }: ProgressProps) {
107
+ const { colors, dark } = useKit();
108
+ const reduce = useReduceMotion();
109
+ const top = max ?? (variant === 'steps' ? steps : 100);
110
+ const unknown = indeterminate || value === undefined;
111
+ const v = clamp(value ?? 0, 0, top);
112
+ const ratio = top > 0 ? v / top : 0;
113
+ const complete = !unknown && top > 0 && v >= top;
114
+ const done = complete && celebrate;
115
+ const tint = color ?? colors.accent;
116
+ const track = trackColor ?? alpha(colors.foreground, dark ? 0.12 : 0.08);
117
+ const text = formatValue ? formatValue(v, top) : `${Math.round(ratio * 100)}%`;
118
+ const a11y = {
119
+ accessibilityRole: 'progressbar' as const,
120
+ accessibilityLabel: label,
121
+ accessibilityState: { busy: unknown },
122
+ accessibilityValue: unknown ? undefined : { min: 0, max: top, now: v, text },
123
+ };
124
+
125
+ if (variant === 'circular') {
126
+ return (
127
+ <View {...a11y} style={[styles.ringRoot, style]}>
128
+ <Ring
129
+ ratio={ratio}
130
+ unknown={unknown}
131
+ size={size}
132
+ tint={tint}
133
+ track={track}
134
+ showValue={showValue ?? !unknown}
135
+ format={formatValue ? () => text : undefined}
136
+ fg={colors.foreground}
137
+ done={done}
138
+ reduce={reduce}
139
+ />
140
+ {label ? <Text style={[styles.ringLabel, { color: colors.mutedForeground }]}>{label}</Text> : null}
141
+ </View>
142
+ );
143
+ }
144
+
145
+ const valueShown = showValue && !unknown;
146
+ const checkShown = done && (!!label || !!showValue);
147
+ const header =
148
+ label || valueShown ? (
149
+ <View style={styles.header}>
150
+ <Text numberOfLines={1} style={[styles.label, { color: colors.foreground }]}>
151
+ {label}
152
+ </Text>
153
+ {checkShown ? (
154
+ <DrawnCheck color={tint} size={18} />
155
+ ) : valueShown ? (
156
+ <Text style={[styles.value, { color: colors.mutedForeground }]}>{text}</Text>
157
+ ) : null}
158
+ </View>
159
+ ) : null;
160
+
161
+ return (
162
+ <View {...a11y} style={style}>
163
+ {header}
164
+ {variant === 'steps' ? (
165
+ <Steps
166
+ count={Math.max(1, Math.round(steps))}
167
+ done={unknown ? 0 : ratio * Math.max(1, Math.round(steps))}
168
+ height={BAR[size]}
169
+ tint={tint}
170
+ track={track}
171
+ finished={complete}
172
+ reduce={reduce}
173
+ />
174
+ ) : (
175
+ <Bar
176
+ ratio={ratio}
177
+ unknown={unknown}
178
+ height={BAR[size]}
179
+ tint={tint}
180
+ track={track}
181
+ striped={striped}
182
+ done={done}
183
+ reduce={reduce}
184
+ />
185
+ )}
186
+ </View>
187
+ );
188
+ }
189
+
190
+ /** Soft radial light in the fill colour, fading to transparent. */
191
+ function Glow({ size, color, id }: { size: number; color: string; id: string }) {
192
+ return (
193
+ <Svg width={size} height={size}>
194
+ <Defs>
195
+ <RadialGradient id={id} cx="50%" cy="50%" r="50%">
196
+ <Stop offset="0" stopColor={color} stopOpacity={0.6} />
197
+ <Stop offset="0.45" stopColor={color} stopOpacity={0.22} />
198
+ <Stop offset="1" stopColor={color} stopOpacity={0} />
199
+ </RadialGradient>
200
+ </Defs>
201
+ <Circle cx={size / 2} cy={size / 2} r={size / 2} fill={`url(#${id})`} />
202
+ </Svg>
203
+ );
204
+ }
205
+
206
+ /** Circle + tick that draw themselves (stroke offset, JS driver) inside a springy wrapper (native). */
207
+ function DrawnCheck({ color, size, ring = true }: { color: string; size: number; ring?: boolean }) {
208
+ const draw = useRef(new Animated.Value(0)).current;
209
+ const pop = useRef(new Animated.Value(0.6)).current;
210
+ useEffect(() => {
211
+ Animated.timing(draw, {
212
+ toValue: 1,
213
+ duration: 560,
214
+ easing: Easing.out(Easing.cubic),
215
+ useNativeDriver: false,
216
+ }).start();
217
+ Animated.spring(pop, {
218
+ toValue: 1,
219
+ useNativeDriver: true,
220
+ ...motion.bouncy,
221
+ }).start();
222
+ }, [draw, pop]);
223
+ const circle = draw.interpolate({
224
+ inputRange: [0, 0.6],
225
+ outputRange: [57, 0],
226
+ extrapolate: 'clamp',
227
+ });
228
+ const tick = draw.interpolate({
229
+ inputRange: [ring ? 0.45 : 0, 1],
230
+ outputRange: [13, 0],
231
+ extrapolate: 'clamp',
232
+ });
233
+ return (
234
+ <Animated.View style={{ width: size, height: size, transform: [{ scale: pop }] }}>
235
+ <Svg width={size} height={size} viewBox="0 0 24 24">
236
+ {ring ? (
237
+ <>
238
+ <Circle cx={12} cy={12} r={9} fill={alpha(color, 0.14)} />
239
+ <AnimatedCircle
240
+ cx={12}
241
+ cy={12}
242
+ r={9}
243
+ stroke={color}
244
+ strokeWidth={2.2}
245
+ strokeLinecap="round"
246
+ strokeDasharray="57 57"
247
+ strokeDashoffset={circle}
248
+ transform="rotate(-90 12 12)"
249
+ fill="none"
250
+ />
251
+ </>
252
+ ) : null}
253
+ <AnimatedPath
254
+ d={ring ? 'M8 12.4l2.7 2.7L16.2 9.4' : 'M5.5 12.6l4.3 4.3L18.5 7.6'}
255
+ stroke={color}
256
+ strokeWidth={ring ? 2.2 : 2.6}
257
+ strokeLinecap="round"
258
+ strokeLinejoin="round"
259
+ strokeDasharray={ring ? '13 13' : '20 20'}
260
+ strokeDashoffset={ring ? tick : draw.interpolate({ inputRange: [0, 1], outputRange: [20, 0] })}
261
+ fill="none"
262
+ />
263
+ </Svg>
264
+ </Animated.View>
265
+ );
266
+ }
267
+
268
+ function Bar({
269
+ ratio,
270
+ unknown,
271
+ height,
272
+ tint,
273
+ track,
274
+ striped,
275
+ done,
276
+ reduce,
277
+ }: {
278
+ ratio: number;
279
+ unknown: boolean;
280
+ height: number;
281
+ tint: string;
282
+ track: string;
283
+ striped: boolean;
284
+ done: boolean;
285
+ reduce: boolean;
286
+ }) {
287
+ const { radius } = useKit();
288
+ const id = useId().replace(/[^a-zA-Z0-9]/g, '');
289
+ const [width, setWidth] = useState(0);
290
+ // All native-driven: they only feed transform / opacity.
291
+ const fill = useRef(new Animated.Value(ratio)).current;
292
+ const sweep = useRef(new Animated.Value(0)).current;
293
+ const flash = useRef(new Animated.Value(0)).current;
294
+ const stripe = useRef(new Animated.Value(0)).current;
295
+ const celebrated = useBecameTrue(done);
296
+
297
+ useEffect(() => {
298
+ Animated.spring(fill, {
299
+ toValue: ratio,
300
+ useNativeDriver: true,
301
+ ...motion.gentle,
302
+ }).start();
303
+ }, [ratio, fill]);
304
+
305
+ useEffect(() => {
306
+ if (!unknown) return;
307
+ sweep.setValue(0);
308
+ const loop = Animated.loop(
309
+ Animated.timing(sweep, {
310
+ toValue: 1,
311
+ duration: 1500,
312
+ easing: Easing.inOut(Easing.cubic),
313
+ useNativeDriver: true,
314
+ }),
315
+ );
316
+ loop.start();
317
+ return () => loop.stop();
318
+ }, [unknown, sweep]);
319
+
320
+ useEffect(() => {
321
+ if (!celebrated || reduce) return;
322
+ flash.setValue(0);
323
+ Animated.sequence([
324
+ Animated.timing(flash, {
325
+ toValue: 1,
326
+ duration: 160,
327
+ easing: Easing.out(Easing.quad),
328
+ useNativeDriver: true,
329
+ }),
330
+ Animated.timing(flash, {
331
+ toValue: 0,
332
+ duration: 620,
333
+ easing: Easing.inOut(Easing.quad),
334
+ useNativeDriver: true,
335
+ }),
336
+ ]).start();
337
+ }, [celebrated, reduce, flash]);
338
+
339
+ const stripesOn = striped && !unknown;
340
+ useEffect(() => {
341
+ stripe.setValue(0);
342
+ if (!stripesOn || reduce) return;
343
+ const loop = Animated.loop(
344
+ Animated.timing(stripe, {
345
+ toValue: 1,
346
+ duration: 700,
347
+ easing: Easing.linear,
348
+ useNativeDriver: true,
349
+ }),
350
+ );
351
+ loop.start();
352
+ return () => loop.stop();
353
+ }, [stripesOn, reduce, stripe]);
354
+
355
+ const corner = radius.capsule(height);
356
+ const glow = Math.max(18, height * 3.4);
357
+ const comet = width * 0.42;
358
+ const period = Math.max(10, height * 2.2);
359
+
360
+ // The fill is full width and slides in from the left, so its rounded end never distorts.
361
+ const translateX = unknown
362
+ ? sweep.interpolate({ inputRange: [0, 1], outputRange: [-comet, width] })
363
+ : fill.interpolate({ inputRange: [0, 1], outputRange: [-width, 0] });
364
+ const glowX = unknown
365
+ ? sweep.interpolate({
366
+ inputRange: [0, 1],
367
+ outputRange: [-height / 2 - glow / 2, width + comet - height / 2 - glow / 2],
368
+ })
369
+ : fill.interpolate({
370
+ inputRange: [0, 1],
371
+ outputRange: [-height / 2 - glow / 2, width - height / 2 - glow / 2],
372
+ });
373
+ const glowOpacity = unknown
374
+ ? sweep.interpolate({
375
+ inputRange: [0, 0.15, 0.8, 1],
376
+ outputRange: [0, 1, 1, 0],
377
+ })
378
+ : fill.interpolate({
379
+ inputRange: [0, 0.03, 1],
380
+ outputRange: [0, 1, 1],
381
+ extrapolate: 'clamp',
382
+ });
383
+
384
+ return (
385
+ <View onLayout={(e) => setWidth(e.nativeEvent.layout.width)} style={styles.barWrap}>
386
+ <View style={[styles.track, { height, borderRadius: corner, backgroundColor: track }]}>
387
+ {width > 0 ? (
388
+ <Animated.View
389
+ style={[
390
+ styles.fill,
391
+ {
392
+ width: unknown ? comet : width,
393
+ height,
394
+ borderRadius: corner,
395
+ transform: [{ translateX }],
396
+ },
397
+ ]}
398
+ >
399
+ {unknown ? (
400
+ // The sweeping comet's tail fades out in the same colour (alpha only, no second hue).
401
+ <Svg width={comet} height={height} style={StyleSheet.absoluteFill}>
402
+ <Defs>
403
+ <LinearGradient id={`${id}fill`} x1="0" y1="0" x2="1" y2="0">
404
+ <Stop offset="0" stopColor={tint} stopOpacity={0} />
405
+ <Stop offset="0.6" stopColor={tint} stopOpacity={0.85} />
406
+ <Stop offset="1" stopColor={tint} stopOpacity={1} />
407
+ </LinearGradient>
408
+ </Defs>
409
+ <Rect width="100%" height="100%" fill={`url(#${id}fill)`} />
410
+ </Svg>
411
+ ) : (
412
+ <View style={[StyleSheet.absoluteFill, { backgroundColor: tint }]} />
413
+ )}
414
+ {stripesOn ? (
415
+ <Animated.View
416
+ style={[
417
+ styles.stripes,
418
+ {
419
+ left: -period,
420
+ width: width + period * 2,
421
+ transform: [
422
+ {
423
+ translateX: stripe.interpolate({
424
+ inputRange: [0, 1],
425
+ outputRange: [0, period],
426
+ }),
427
+ },
428
+ ],
429
+ },
430
+ ]}
431
+ >
432
+ <Svg width={width + period * 2} height={height}>
433
+ {Array.from({ length: Math.ceil(width / period) + 3 }, (_, i) => {
434
+ const x = i * period - height;
435
+ return (
436
+ <Polygon
437
+ key={i}
438
+ points={`${x},${height} ${x + height},0 ${x + height + period / 2},0 ${x + period / 2},${height}`}
439
+ fill={alpha(readableOn(tint), 0.2)}
440
+ />
441
+ );
442
+ })}
443
+ </Svg>
444
+ </Animated.View>
445
+ ) : null}
446
+ <Animated.View
447
+ style={[
448
+ StyleSheet.absoluteFill,
449
+ {
450
+ backgroundColor: WHITE,
451
+ opacity: flash.interpolate({
452
+ inputRange: [0, 1],
453
+ outputRange: [0, 0.45],
454
+ }),
455
+ },
456
+ ]}
457
+ />
458
+ </Animated.View>
459
+ ) : null}
460
+ </View>
461
+ {width > 0 ? (
462
+ <Animated.View
463
+ pointerEvents="none"
464
+ style={[
465
+ styles.glow,
466
+ {
467
+ top: (height - glow) / 2,
468
+ opacity: glowOpacity,
469
+ transform: [
470
+ { translateX: glowX },
471
+ {
472
+ scale: flash.interpolate({
473
+ inputRange: [0, 1],
474
+ outputRange: [1, 1.7],
475
+ }),
476
+ },
477
+ ],
478
+ },
479
+ ]}
480
+ >
481
+ <Glow size={glow} color={tint} id={`${id}glow`} />
482
+ </Animated.View>
483
+ ) : null}
484
+ </View>
485
+ );
486
+ }
487
+
488
+ function Steps({
489
+ count,
490
+ done,
491
+ height,
492
+ tint,
493
+ track,
494
+ finished,
495
+ reduce,
496
+ }: {
497
+ count: number;
498
+ done: number;
499
+ height: number;
500
+ tint: string;
501
+ track: string;
502
+ finished: boolean;
503
+ reduce: boolean;
504
+ }) {
505
+ // Remember where the last change started so the segments that change fill one after another.
506
+ const [prev, setPrev] = useState(done);
507
+ const [from, setFrom] = useState(done);
508
+ if (done !== prev) {
509
+ setFrom(prev);
510
+ setPrev(done);
511
+ }
512
+ const current = finished || done >= count ? -1 : Math.floor(done);
513
+ return (
514
+ <View style={[styles.steps, { gap: Math.max(4, height * 0.75) }]}>
515
+ {Array.from({ length: count }, (_, i) => {
516
+ const delay =
517
+ done >= from ? Math.max(0, i - Math.floor(from)) * 110 : Math.max(0, Math.ceil(from) - 1 - i) * 70;
518
+ return (
519
+ <Segment
520
+ key={i}
521
+ fill={clamp(done - i, 0, 1)}
522
+ delay={delay}
523
+ height={height}
524
+ tint={tint}
525
+ track={track}
526
+ pulse={i === current && !reduce}
527
+ reduce={reduce}
528
+ />
529
+ );
530
+ })}
531
+ </View>
532
+ );
533
+ }
534
+
535
+ function Segment({
536
+ fill,
537
+ delay,
538
+ height,
539
+ tint,
540
+ track,
541
+ pulse,
542
+ reduce,
543
+ }: {
544
+ fill: number;
545
+ delay: number;
546
+ height: number;
547
+ tint: string;
548
+ track: string;
549
+ pulse: boolean;
550
+ reduce: boolean;
551
+ }) {
552
+ const { radius } = useKit();
553
+ const v = useRef(new Animated.Value(fill)).current;
554
+ const pop = useRef(new Animated.Value(1)).current;
555
+ const glow = useRef(new Animated.Value(0)).current;
556
+ const prevFill = useRef(fill);
557
+
558
+ useEffect(() => {
559
+ // A segment that just became full gets a little pop as its fill lands.
560
+ const popping = fill >= 1 && prevFill.current < 1 && !reduce;
561
+ prevFill.current = fill;
562
+ const anim = Animated.sequence([
563
+ Animated.delay(delay),
564
+ Animated.parallel([
565
+ Animated.spring(v, {
566
+ toValue: fill,
567
+ useNativeDriver: true,
568
+ ...motion.snappy,
569
+ }),
570
+ popping
571
+ ? Animated.sequence([
572
+ Animated.delay(120),
573
+ Animated.timing(pop, {
574
+ toValue: 1.18,
575
+ duration: 110,
576
+ easing: Easing.out(Easing.quad),
577
+ useNativeDriver: true,
578
+ }),
579
+ Animated.spring(pop, {
580
+ toValue: 1,
581
+ useNativeDriver: true,
582
+ ...motion.bouncy,
583
+ }),
584
+ ])
585
+ : Animated.delay(0),
586
+ ]),
587
+ ]);
588
+ anim.start();
589
+ return () => anim.stop();
590
+ }, [fill, delay, reduce, v, pop]);
591
+
592
+ useEffect(() => {
593
+ glow.setValue(0);
594
+ if (!pulse) return;
595
+ const loop = Animated.loop(
596
+ Animated.sequence([
597
+ Animated.timing(glow, {
598
+ toValue: 1,
599
+ duration: 750,
600
+ easing: Easing.inOut(Easing.sin),
601
+ useNativeDriver: true,
602
+ }),
603
+ Animated.timing(glow, {
604
+ toValue: 0,
605
+ duration: 750,
606
+ easing: Easing.inOut(Easing.sin),
607
+ useNativeDriver: true,
608
+ }),
609
+ ]),
610
+ );
611
+ loop.start();
612
+ return () => loop.stop();
613
+ }, [pulse, glow]);
614
+
615
+ return (
616
+ <Animated.View
617
+ style={[
618
+ styles.segment,
619
+ {
620
+ height,
621
+ borderRadius: radius.capsule(height),
622
+ backgroundColor: track,
623
+ transform: [
624
+ { scaleY: pop },
625
+ {
626
+ scaleX: pop.interpolate({
627
+ inputRange: [1, 1.18],
628
+ outputRange: [1, 1.04],
629
+ }),
630
+ },
631
+ ],
632
+ },
633
+ ]}
634
+ >
635
+ <Animated.View
636
+ style={[
637
+ StyleSheet.absoluteFill,
638
+ {
639
+ backgroundColor: tint,
640
+ opacity: glow.interpolate({
641
+ inputRange: [0, 1],
642
+ outputRange: [0, 0.3],
643
+ }),
644
+ },
645
+ ]}
646
+ />
647
+ <Animated.View
648
+ style={[StyleSheet.absoluteFill, styles.segmentFill, { backgroundColor: tint, transform: [{ scaleX: v }] }]}
649
+ />
650
+ </Animated.View>
651
+ );
652
+ }
653
+
654
+ /** The ring's live percentage. Owns the per-frame listener so only this text re-renders, not the SVG. */
655
+ function RingPercent({ fill, initial }: { fill: Animated.Value; initial: number }) {
656
+ const [shown, setShown] = useState(Math.round(initial * 100));
657
+ useEffect(() => {
658
+ const listener = fill.addListener(({ value }) => setShown(Math.round(clamp(value, 0, 1) * 100)));
659
+ return () => fill.removeListener(listener);
660
+ }, [fill]);
661
+ return <>{shown}</>;
662
+ }
663
+
664
+ function Ring({
665
+ ratio,
666
+ unknown,
667
+ size,
668
+ tint,
669
+ track,
670
+ showValue,
671
+ format,
672
+ fg,
673
+ done,
674
+ reduce,
675
+ }: {
676
+ ratio: number;
677
+ unknown: boolean;
678
+ size: ProgressSize;
679
+ tint: string;
680
+ track: string;
681
+ showValue: boolean;
682
+ format?: () => string;
683
+ fg: string;
684
+ done: boolean;
685
+ reduce: boolean;
686
+ }) {
687
+ const { d, w, font } = RING[size];
688
+ const id = useId().replace(/[^a-zA-Z0-9]/g, '');
689
+ const r = (d - w) / 2;
690
+ const c = 2 * Math.PI * r;
691
+ const glow = Math.max(14, w * 3.4);
692
+ const target = unknown ? 0.25 : ratio;
693
+ // `fill` drives the SVG stroke (JS driver). `turn` mirrors it on the native driver to carry the
694
+ // glowing dot, and `spin` / `morph` / `flash` are native too — each on its own view.
695
+ const fill = useRef(new Animated.Value(target)).current;
696
+ const turn = useRef(new Animated.Value(target)).current;
697
+ const spin = useRef(new Animated.Value(0)).current;
698
+ const morph = useRef(new Animated.Value(done ? 1 : 0)).current;
699
+ const flash = useRef(new Animated.Value(0)).current;
700
+ const celebrated = useBecameTrue(done);
701
+
702
+ useEffect(() => {
703
+ Animated.parallel([
704
+ Animated.spring(fill, {
705
+ toValue: target,
706
+ useNativeDriver: false,
707
+ ...motion.gentle,
708
+ }),
709
+ Animated.spring(turn, {
710
+ toValue: target,
711
+ useNativeDriver: true,
712
+ ...motion.gentle,
713
+ }),
714
+ ]).start();
715
+ }, [target, fill, turn]);
716
+
717
+ useEffect(() => {
718
+ Animated.spring(morph, {
719
+ toValue: done ? 1 : 0,
720
+ useNativeDriver: true,
721
+ ...motion.snappy,
722
+ }).start();
723
+ }, [done, morph]);
724
+
725
+ useEffect(() => {
726
+ if (!celebrated || reduce) return;
727
+ flash.setValue(0);
728
+ Animated.sequence([
729
+ Animated.timing(flash, {
730
+ toValue: 1,
731
+ duration: 160,
732
+ useNativeDriver: true,
733
+ }),
734
+ Animated.timing(flash, {
735
+ toValue: 0,
736
+ duration: 620,
737
+ easing: Easing.inOut(Easing.quad),
738
+ useNativeDriver: true,
739
+ }),
740
+ ]).start();
741
+ }, [celebrated, reduce, flash]);
742
+
743
+ useEffect(() => {
744
+ spin.setValue(0);
745
+ if (!unknown) return;
746
+ const loop = Animated.loop(
747
+ Animated.timing(spin, {
748
+ toValue: 1,
749
+ duration: 900,
750
+ easing: Easing.linear,
751
+ useNativeDriver: true,
752
+ }),
753
+ );
754
+ loop.start();
755
+ return () => loop.stop();
756
+ }, [unknown, spin]);
757
+
758
+ const offset = fill.interpolate({
759
+ inputRange: [0, 1],
760
+ outputRange: [c, 0],
761
+ extrapolate: 'clamp',
762
+ });
763
+ const rotate = spin.interpolate({
764
+ inputRange: [0, 1],
765
+ outputRange: ['0deg', '360deg'],
766
+ });
767
+ const dotTurn = turn.interpolate({
768
+ inputRange: [0, 1],
769
+ outputRange: ['0deg', '360deg'],
770
+ });
771
+
772
+ return (
773
+ <View style={{ width: d, height: d }}>
774
+ <Animated.View style={[StyleSheet.absoluteFill, { transform: [{ rotate }] }]}>
775
+ {/* Rotated -90° so the stroke starts at 12 o'clock. */}
776
+ <View style={styles.startAtTop}>
777
+ <Svg width={d} height={d}>
778
+ <Circle cx={d / 2} cy={d / 2} r={r} stroke={track} strokeWidth={w} fill="none" />
779
+ <AnimatedCircle
780
+ cx={d / 2}
781
+ cy={d / 2}
782
+ r={r}
783
+ stroke={tint}
784
+ strokeWidth={w}
785
+ strokeLinecap="round"
786
+ strokeDasharray={`${c} ${c}`}
787
+ strokeDashoffset={offset}
788
+ fill="none"
789
+ />
790
+ </Svg>
791
+ </View>
792
+ {/* The glowing dot rides the end of the arc. */}
793
+ <Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { transform: [{ rotate: dotTurn }] }]}>
794
+ <Animated.View
795
+ style={[
796
+ styles.ringDot,
797
+ {
798
+ left: d / 2 - glow / 2,
799
+ top: w / 2 - glow / 2,
800
+ opacity: turn.interpolate({
801
+ inputRange: [0, 0.02, 1],
802
+ outputRange: [0, 1, 1],
803
+ extrapolate: 'clamp',
804
+ }),
805
+ transform: [
806
+ {
807
+ scale: flash.interpolate({
808
+ inputRange: [0, 1],
809
+ outputRange: [1, 1.8],
810
+ }),
811
+ },
812
+ ],
813
+ },
814
+ ]}
815
+ >
816
+ <Glow size={glow} color={tint} id={`${id}dot`} />
817
+ </Animated.View>
818
+ </Animated.View>
819
+ </Animated.View>
820
+ {showValue && !unknown ? (
821
+ <View style={[StyleSheet.absoluteFill, styles.center]} pointerEvents="none">
822
+ <Animated.View
823
+ style={{
824
+ opacity: morph.interpolate({
825
+ inputRange: [0, 1],
826
+ outputRange: [1, 0],
827
+ }),
828
+ transform: [
829
+ {
830
+ scale: morph.interpolate({
831
+ inputRange: [0, 1],
832
+ outputRange: [1, 0.5],
833
+ }),
834
+ },
835
+ ],
836
+ }}
837
+ >
838
+ <Text style={[styles.ringValue, { color: fg, fontSize: font }]}>
839
+ {format ? format() : <RingPercent fill={fill} initial={ratio} />}
840
+ {format ? null : <Text style={{ fontSize: font * 0.6, color: alpha(fg, 0.55) }}>%</Text>}
841
+ </Text>
842
+ </Animated.View>
843
+ {done ? (
844
+ <Animated.View style={[StyleSheet.absoluteFill, styles.center, { opacity: morph }]}>
845
+ <DrawnCheck color={tint} size={Math.round(font * 1.7)} ring={false} />
846
+ </Animated.View>
847
+ ) : null}
848
+ </View>
849
+ ) : null}
850
+ </View>
851
+ );
852
+ }
853
+
854
+ const styles = StyleSheet.create({
855
+ header: {
856
+ flexDirection: 'row',
857
+ alignItems: 'center',
858
+ justifyContent: 'space-between',
859
+ gap: 12,
860
+ marginBottom: 8,
861
+ minHeight: 18,
862
+ },
863
+ label: { flexShrink: 1, fontSize: 14, fontWeight: '400' },
864
+ value: { fontSize: 13, fontWeight: '400', fontVariant: ['tabular-nums'] },
865
+ barWrap: { alignSelf: 'stretch' },
866
+ track: { overflow: 'hidden', alignSelf: 'stretch' },
867
+ fill: { overflow: 'hidden' },
868
+ stripes: { position: 'absolute', top: 0, bottom: 0 },
869
+ glow: { position: 'absolute', left: 0 },
870
+ steps: { flexDirection: 'row', alignSelf: 'stretch' },
871
+ segment: { flex: 1, overflow: 'hidden' },
872
+ segmentFill: { transformOrigin: 'left center' },
873
+ ringRoot: { alignItems: 'center', alignSelf: 'flex-start', gap: 8 },
874
+ ringLabel: { fontSize: 13, fontWeight: '400' },
875
+ ringDot: { position: 'absolute' },
876
+ center: { alignItems: 'center', justifyContent: 'center' },
877
+ startAtTop: { transform: [{ rotate: '-90deg' }] },
878
+ ringValue: {
879
+ fontWeight: '400',
880
+ letterSpacing: -0.4,
881
+ fontVariant: ['tabular-nums'],
882
+ },
883
+ });