@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,748 @@
1
+ import React, { useEffect, 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 } from 'react-native-svg';
13
+
14
+ import { alpha, useKit } from '@/lib/kit';
15
+
16
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
17
+
18
+ export type SpinnerVariant =
19
+ | 'ring'
20
+ | 'dots'
21
+ | 'bars'
22
+ | 'pulse'
23
+ | 'orbit'
24
+ | 'bounce'
25
+ | 'wave'
26
+ | 'grid'
27
+ | 'typing'
28
+ | 'arc'
29
+ | 'square'
30
+ | 'line';
31
+ export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl';
32
+ export type SpinnerLabelPosition = 'none' | 'right' | 'bottom';
33
+
34
+ export interface SpinnerProps {
35
+ variant?: SpinnerVariant;
36
+ /** 'sm' 16 · 'md' 24 · 'lg' 36 · 'xl' 52 — or any number of points. */
37
+ size?: SpinnerSize | number;
38
+ /** Indicator colour (default: theme accent). */
39
+ color?: string;
40
+ /** Speed multiplier: 2 is twice as fast. */
41
+ speed?: number;
42
+ /** Read by screen readers, and shown when `labelPosition` is set. */
43
+ label?: string;
44
+ /** Show `label` beside or under the spinner with an animated ellipsis (default 'none': screen readers only). */
45
+ labelPosition?: SpinnerLabelPosition;
46
+ /** Faint guide behind the motion: the full circle (ring, orbit, arc), the rail (line) or ghost bars (bars). */
47
+ track?: boolean;
48
+ style?: StyleProp<ViewStyle>;
49
+ }
50
+
51
+ const SIZES: Record<SpinnerSize, number> = { sm: 16, md: 24, lg: 36, xl: 52 };
52
+
53
+ /** The indicator's box: a `size` square, except the line — a thin bar `size * 4` wide. */
54
+ function boxOf(variant: SpinnerVariant, px: number) {
55
+ return variant === 'line' ? { width: px * 4, height: lineThickness(px) } : { width: px, height: px };
56
+ }
57
+
58
+ function lineThickness(px: number) {
59
+ return Math.max(2, Math.round(px / 8));
60
+ }
61
+ /** Arcs stacked from the head backwards: the shortest is solid, the rest faint, so the tail fades out. */
62
+ const TAIL = 7;
63
+
64
+ function useReduceMotion() {
65
+ const [reduce, setReduce] = useState(false);
66
+ useEffect(() => {
67
+ let alive = true;
68
+ AccessibilityInfo.isReduceMotionEnabled()
69
+ .then((r) => alive && setReduce(r))
70
+ .catch(() => {});
71
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
72
+ return () => {
73
+ alive = false;
74
+ sub.remove();
75
+ };
76
+ }, []);
77
+ return reduce;
78
+ }
79
+
80
+ /** Looping value 0 → 1. Each spinner part interpolates it. */
81
+ function useLoop(duration: number, easing: (t: number) => number, native: boolean, running = true) {
82
+ const v = useRef(new Animated.Value(0)).current;
83
+ useEffect(() => {
84
+ v.setValue(0);
85
+ if (!running) return;
86
+ const loop = Animated.loop(
87
+ Animated.timing(v, {
88
+ toValue: 1,
89
+ duration,
90
+ easing,
91
+ useNativeDriver: native,
92
+ }),
93
+ );
94
+ loop.start();
95
+ return () => loop.stop();
96
+ }, [v, duration, easing, native, running]);
97
+ return v;
98
+ }
99
+
100
+ const linear = Easing.linear;
101
+ const sine = Easing.inOut(Easing.sin);
102
+
103
+ /**
104
+ * Pre-samples an eased piecewise curve (each segment eased like `interpolate({ easing })`) so it can
105
+ * run on the native driver, which doesn't support `easing` on interpolations.
106
+ */
107
+ function eased(points: number[], values: number[], ease: (t: number) => number, steps = 12) {
108
+ const inputRange: number[] = [];
109
+ const outputRange: number[] = [];
110
+ for (let i = 0; i < points.length - 1; i++) {
111
+ for (let k = 0; k < steps; k++) {
112
+ const t = k / steps;
113
+ inputRange.push(points[i] + (points[i + 1] - points[i]) * t);
114
+ outputRange.push(values[i] + (values[i + 1] - values[i]) * ease(t));
115
+ }
116
+ }
117
+ inputRange.push(points[points.length - 1]);
118
+ outputRange.push(values[values.length - 1]);
119
+ return { inputRange, outputRange };
120
+ }
121
+
122
+ /**
123
+ * Samples a periodic curve `f(t)`, t ∈ [0, 1], into an interpolation a native-driver loop can play.
124
+ * Each part passes its own phase into `f`, so one loop drives a whole staggered pattern.
125
+ */
126
+ function curve(f: (t: number) => number, steps = 24) {
127
+ const inputRange: number[] = [];
128
+ const outputRange: number[] = [];
129
+ for (let k = 0; k <= steps; k++) {
130
+ inputRange.push(k / steps);
131
+ outputRange.push(f(k / steps));
132
+ }
133
+ return { inputRange, outputRange };
134
+ }
135
+
136
+ /** Same as `curve`, for rotations. */
137
+ function curveDeg(f: (t: number) => number, steps = 24) {
138
+ const r = curve(f, steps);
139
+ return { inputRange: r.inputRange, outputRange: r.outputRange.map((d) => `${d}deg`) };
140
+ }
141
+
142
+ /** Wraps any number into [0, 1) — `wrap(t - phase)` delays a part by `phase` of a cycle. */
143
+ const wrap = (t: number) => t - Math.floor(t);
144
+ /** 0 → 1 → 0 hump over the first `width` of a cycle, flat afterwards. */
145
+ const hump = (u: number, width: number) => (u < width ? Math.sin((Math.PI * u) / width) : 0);
146
+ const cubic = Easing.inOut(Easing.cubic);
147
+ const outCubic = Easing.out(Easing.cubic);
148
+
149
+ /**
150
+ * Loading indicator — a comet arc whose tail fades out and breathes as it spins, a soft wave of dots,
151
+ * or one of ten more loaders (bars, pulse, orbit, bounce, wave, grid, typing, arc, square, line).
152
+ * Optionally shows a label with an animated ellipsis. Motion runs on the native driver.
153
+ */
154
+ export function Spinner({
155
+ variant = 'ring',
156
+ size = 'md',
157
+ color,
158
+ speed = 1,
159
+ label = 'Loading',
160
+ labelPosition = 'none',
161
+ track = false,
162
+ style,
163
+ }: SpinnerProps) {
164
+ const { colors } = useKit();
165
+ const reduce = useReduceMotion();
166
+ const px = typeof size === 'number' ? size : SIZES[size];
167
+ const tint = color ?? colors.accent;
168
+ const Body = BODIES[variant] ?? Ring;
169
+ const s = Math.max(0.1, speed);
170
+ const body = <Body size={px} color={tint} speed={s} track={track} reduce={reduce} />;
171
+ const box = boxOf(BODIES[variant] ? variant : 'ring', px);
172
+
173
+ if (labelPosition === 'none' || !label) {
174
+ return (
175
+ <View
176
+ accessible
177
+ accessibilityRole="progressbar"
178
+ accessibilityLabel={label}
179
+ accessibilityState={{ busy: true }}
180
+ style={[styles.root, box, style]}
181
+ >
182
+ {body}
183
+ </View>
184
+ );
185
+ }
186
+
187
+ const bottom = labelPosition === 'bottom';
188
+ return (
189
+ <View
190
+ accessible
191
+ accessibilityRole="progressbar"
192
+ accessibilityLabel={label}
193
+ accessibilityState={{ busy: true }}
194
+ style={[bottom ? styles.column : styles.inline, { gap: bottom ? 10 : 8 }, style]}
195
+ >
196
+ <View style={[styles.root, box]}>{body}</View>
197
+ <Ellipsis
198
+ text={label}
199
+ speed={s}
200
+ reduce={reduce}
201
+ color={colors.mutedForeground}
202
+ fontSize={bottom ? 14 : Math.max(13, Math.min(15, px * 0.62))}
203
+ />
204
+ </View>
205
+ );
206
+ }
207
+
208
+ interface BodyProps {
209
+ size: number;
210
+ color: string;
211
+ speed: number;
212
+ track: boolean;
213
+ reduce: boolean;
214
+ }
215
+
216
+ function Ring({ size, color, speed, track, reduce }: BodyProps) {
217
+ const stroke = Math.max(2, size / 9);
218
+ const r = (size - stroke) / 2;
219
+ const c = 2 * Math.PI * r;
220
+ // Native: a steady spin plus an eased surge on a nested view. JS: the arc length breathing on the SVG.
221
+ const spin = useLoop(1100 / speed, linear, true);
222
+ const lunge = useLoop(1500 / speed, linear, true, !reduce);
223
+ const breath = useLoop(1500 / speed, linear, false, !reduce);
224
+
225
+ const rotate = spin.interpolate({
226
+ inputRange: [0, 1],
227
+ outputRange: ['0deg', '360deg'],
228
+ });
229
+ // The head races ahead while the arc grows, then eases while the tail catches up.
230
+ const surge = eased([0, 0.5, 1], [0, 290, 360], sine);
231
+ const rotateSurge = lunge.interpolate({ inputRange: surge.inputRange, outputRange: surge.outputRange.map((d) => `${d}deg`) });
232
+ // Arc length breathes 14% → 62% → 14% of the circle.
233
+ const min = c * 0.14;
234
+ const max = c * 0.62;
235
+ const steady = c * 0.36;
236
+
237
+ return (
238
+ <Animated.View style={{ width: size, height: size, transform: [{ rotate }] }}>
239
+ <Animated.View style={[StyleSheet.absoluteFill, { transform: [{ rotate: rotateSurge }] }]}>
240
+ {/* Mirrored so the dash grows backwards from a fixed head at the path start. */}
241
+ <View style={styles.mirror}>
242
+ <Svg width={size} height={size}>
243
+ {track ? (
244
+ <Circle cx={size / 2} cy={size / 2} r={r} stroke={alpha(color, 0.14)} strokeWidth={stroke} fill="none" />
245
+ ) : null}
246
+ {Array.from({ length: TAIL }, (_, k) => {
247
+ // k = 0 is the full tail (faintest layer), k = TAIL - 1 the solid head.
248
+ const part = (TAIL - k) / TAIL;
249
+ const headLayer = k === TAIL - 1;
250
+ const longest = max * part;
251
+ const offset = reduce
252
+ ? longest - steady * part
253
+ : breath.interpolate({
254
+ inputRange: [0, 0.5, 1],
255
+ outputRange: [longest - min * part, 0, longest - min * part],
256
+ easing: sine,
257
+ });
258
+ return (
259
+ <AnimatedCircle
260
+ key={k}
261
+ cx={size / 2}
262
+ cy={size / 2}
263
+ r={r}
264
+ stroke={color}
265
+ strokeOpacity={headLayer ? 1 : 0.2}
266
+ strokeWidth={stroke}
267
+ strokeLinecap="round"
268
+ strokeDasharray={`${longest} ${c * 2}`}
269
+ strokeDashoffset={offset}
270
+ fill="none"
271
+ />
272
+ );
273
+ })}
274
+ </Svg>
275
+ </View>
276
+ </Animated.View>
277
+ </Animated.View>
278
+ );
279
+ }
280
+
281
+ function Dots({ size, color, speed, reduce }: BodyProps) {
282
+ const t = useLoop(1200 / speed, linear, true, !reduce);
283
+ const dot = size / 4.2;
284
+ return (
285
+ <View style={[styles.row, { gap: dot / 2.2 }]}>
286
+ {[0, 1, 2].map((i) => {
287
+ // A smooth wave: each dot swells, brightens and lifts slightly during its slice of the cycle.
288
+ const s = i * 0.16;
289
+ // Pre-sampled with eased(): the native driver rejects `easing` on interpolations.
290
+ const wave = (values: number[]) => eased([0, s, s + 0.22, s + 0.44, 1], values, sine);
291
+ return (
292
+ <Animated.View
293
+ key={i}
294
+ style={{
295
+ width: dot,
296
+ height: dot,
297
+ borderRadius: dot / 2,
298
+ backgroundColor: color,
299
+ opacity: reduce
300
+ ? 0.4 + i * 0.3
301
+ : t.interpolate(wave([0.35, 0.35, 1, 0.35, 0.35])),
302
+ transform: reduce
303
+ ? []
304
+ : [
305
+ {
306
+ translateY: t.interpolate(wave([0, 0, -size / 9, 0, 0])),
307
+ },
308
+ {
309
+ scale: t.interpolate(wave([0.65, 0.65, 1.1, 0.65, 0.65])),
310
+ },
311
+ ],
312
+ }}
313
+ />
314
+ );
315
+ })}
316
+ </View>
317
+ );
318
+ }
319
+
320
+ /** Equalizer: four rounded bars bouncing at different rates (native scaleY). */
321
+ function Bars({ size, color, speed, track, reduce }: BodyProps) {
322
+ const { radius } = useKit();
323
+ const t = useLoop(1300 / speed, linear, true, !reduce);
324
+ const w = size / 6.4;
325
+ const h = size * 0.78;
326
+ const corner = radius.capsule(w);
327
+ return (
328
+ <View style={[styles.row, { gap: w * 0.6, height: h }]}>
329
+ {BAR_RATE.map((rate, i) => {
330
+ const scaleY = reduce
331
+ ? BAR_REST[i]
332
+ : t.interpolate(curve((u) => 0.28 + 0.72 * Math.abs(Math.sin(Math.PI * (rate * u + BAR_PHASE[i]))), 32));
333
+ return (
334
+ <View key={i} style={{ width: w, height: h }}>
335
+ {track ? <View style={[StyleSheet.absoluteFill, { borderRadius: corner, backgroundColor: alpha(color, 0.14) }]} /> : null}
336
+ <Animated.View style={[StyleSheet.absoluteFill, { borderRadius: corner, backgroundColor: color, transform: [{ scaleY }] }]} />
337
+ </View>
338
+ );
339
+ })}
340
+ </View>
341
+ );
342
+ }
343
+ const BAR_RATE = [1, 2, 1, 2];
344
+ const BAR_PHASE = [0, 0.2, 0.55, 0.35];
345
+ const BAR_REST = [0.5, 0.9, 0.35, 0.7];
346
+
347
+ /** Two rings ripple out of a breathing core and fade (native scale/opacity). */
348
+ function Pulse({ size, color, speed, track, reduce }: BodyProps) {
349
+ const t = useLoop(1700 / speed, linear, true, !reduce);
350
+ const stroke = Math.max(1.5, size / 14);
351
+ const core = size * 0.34;
352
+ const ring = { width: size, height: size, borderRadius: size / 2, borderWidth: stroke, borderColor: color };
353
+ return (
354
+ <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
355
+ {track ? <View style={[styles.abs, ring, { borderColor: alpha(color, 0.14) }]} /> : null}
356
+ {[0, 0.5].map((phase, i) => (
357
+ <Animated.View
358
+ key={phase}
359
+ style={[
360
+ styles.abs,
361
+ ring,
362
+ reduce
363
+ ? { opacity: i ? 0.25 : 0.55, transform: [{ scale: i ? 1 : 0.68 }] }
364
+ : {
365
+ opacity: t.interpolate(curve((u) => Math.pow(1 - wrap(u - phase), 1.6))),
366
+ transform: [{ scale: t.interpolate(curve((u) => 0.34 + 0.66 * outCubic(wrap(u - phase)))) }],
367
+ },
368
+ ]}
369
+ />
370
+ ))}
371
+ <Animated.View
372
+ style={{
373
+ width: core,
374
+ height: core,
375
+ borderRadius: core / 2,
376
+ backgroundColor: color,
377
+ transform: [{ scale: reduce ? 1 : t.interpolate(curve((u) => 0.86 + 0.14 * Math.cos(2 * Math.PI * u))) }],
378
+ }}
379
+ />
380
+ </View>
381
+ );
382
+ }
383
+
384
+ /** A dot (with a fading tail) laps a faint circle, easing through the top (native rotation). */
385
+ function Orbit({ size, color, speed, track, reduce }: BodyProps) {
386
+ const t = useLoop(1400 / speed, linear, true, !reduce);
387
+ const dot = Math.max(4, size * 0.24);
388
+ const stroke = Math.max(1.5, size / 14);
389
+ // Unwrapped angle, so a lagging tail stays continuous across the loop's restart.
390
+ const angle = (x: number) => 360 * Math.floor(x) + 360 * (0.55 * wrap(x) + 0.45 * sine(wrap(x)));
391
+ return (
392
+ <View style={{ width: size, height: size }}>
393
+ <View
394
+ style={{
395
+ position: 'absolute',
396
+ left: dot / 2,
397
+ top: dot / 2,
398
+ width: size - dot,
399
+ height: size - dot,
400
+ borderRadius: (size - dot) / 2,
401
+ borderWidth: stroke,
402
+ borderColor: alpha(color, track ? 0.28 : 0.16),
403
+ }}
404
+ />
405
+ {[0.08, 0.04, 0].map((lag, i) => (
406
+ <Animated.View
407
+ key={lag}
408
+ style={[
409
+ StyleSheet.absoluteFill,
410
+ {
411
+ opacity: [0.2, 0.4, 1][i],
412
+ transform: [
413
+ { rotate: reduce ? `${45 - lag * 300}deg` : t.interpolate(curveDeg((u) => angle(u - lag), 36)) },
414
+ ],
415
+ },
416
+ ]}
417
+ >
418
+ <View
419
+ style={{
420
+ position: 'absolute',
421
+ top: 0,
422
+ left: (size - dot) / 2,
423
+ width: dot,
424
+ height: dot,
425
+ borderRadius: dot / 2,
426
+ backgroundColor: color,
427
+ transform: [{ scale: [0.6, 0.8, 1][i] }],
428
+ }}
429
+ />
430
+ </Animated.View>
431
+ ))}
432
+ </View>
433
+ );
434
+ }
435
+
436
+ /** Three dots bounce in turn, squashing as they land (native translate/scale). */
437
+ function Bounce({ size, color, speed, reduce }: BodyProps) {
438
+ const t = useLoop(1100 / speed, linear, true, !reduce);
439
+ const dot = size / 5;
440
+ const lift = size * 0.38;
441
+ // Gravity parabola, plus a short squash around each landing.
442
+ const height = (u: number) => 4 * u * (1 - u);
443
+ const squash = (u: number) => Math.max(0, 1 - Math.min(u, 1 - u) / 0.1);
444
+ return (
445
+ <View style={[styles.row, { width: size, height: size, gap: dot / 2, alignItems: 'flex-end', paddingBottom: size * 0.16 }]}>
446
+ {[0, 1, 2].map((i) => {
447
+ const at = (u: number) => wrap(u - i * 0.14);
448
+ const sy = (u: number) => 1 - 0.32 * squash(at(u));
449
+ return (
450
+ <Animated.View
451
+ key={i}
452
+ style={{
453
+ width: dot,
454
+ height: dot,
455
+ borderRadius: dot / 2,
456
+ backgroundColor: color,
457
+ transform: reduce
458
+ ? [{ translateY: -lift * [1, 0.55, 0.15][i] }]
459
+ : [
460
+ // Shift down by the squash so the dot stays on the floor.
461
+ { translateY: t.interpolate(curve((u) => -lift * height(at(u)) + (dot * (1 - sy(u))) / 2, 40)) },
462
+ { scaleX: t.interpolate(curve((u) => 1 + 0.28 * squash(at(u)), 40)) },
463
+ { scaleY: t.interpolate(curve(sy, 40)) },
464
+ ],
465
+ }}
466
+ />
467
+ );
468
+ })}
469
+ </View>
470
+ );
471
+ }
472
+
473
+ /** Five dots ride a travelling sine wave, brightest at the crest (native translate/opacity). */
474
+ function Wave({ size, color, speed, reduce }: BodyProps) {
475
+ const t = useLoop(1200 / speed, linear, true, !reduce);
476
+ const dot = size / 7;
477
+ const amp = size * 0.2;
478
+ return (
479
+ <View style={[styles.row, { gap: dot / 2, height: size }]}>
480
+ {[0, 1, 2, 3, 4].map((i) => {
481
+ const y = (u: number) => Math.sin(2 * Math.PI * (u - i * 0.2));
482
+ return (
483
+ <Animated.View
484
+ key={i}
485
+ style={{
486
+ width: dot,
487
+ height: dot,
488
+ borderRadius: dot / 2,
489
+ backgroundColor: color,
490
+ opacity: reduce ? 0.45 + 0.55 * ((y(0) + 1) / 2) : t.interpolate(curve((u) => 0.4 + 0.6 * ((y(u) + 1) / 2))),
491
+ transform: [{ translateY: reduce ? -amp * y(0) : t.interpolate(curve((u) => -amp * y(u))) }],
492
+ }}
493
+ />
494
+ );
495
+ })}
496
+ </View>
497
+ );
498
+ }
499
+
500
+ /** 3×3 tiles light up in a diagonal sweep (native scale/opacity). */
501
+ function Grid({ size, color, speed, reduce }: BodyProps) {
502
+ const { radius } = useKit();
503
+ const t = useLoop(1300 / speed, linear, true, !reduce);
504
+ const cell = size * 0.27;
505
+ const gap = (size - cell * 3) / 2;
506
+ return (
507
+ <View style={{ width: size, height: size, gap }}>
508
+ {[0, 1, 2].map((row) => (
509
+ <View key={row} style={[styles.row, { gap }]}>
510
+ {[0, 1, 2].map((col) => {
511
+ const d = row + col; // diagonal 0 – 4
512
+ const lit = (u: number) => hump(wrap(u - d * 0.1), 0.5);
513
+ return (
514
+ <Animated.View
515
+ key={col}
516
+ style={{
517
+ width: cell,
518
+ height: cell,
519
+ borderRadius: radius.scale(cell * 0.28),
520
+ backgroundColor: color,
521
+ opacity: reduce ? 1 - d * 0.18 : t.interpolate(curve((u) => 0.22 + 0.78 * lit(u))),
522
+ transform: [{ scale: reduce ? 1 : t.interpolate(curve((u) => 0.62 + 0.38 * lit(u))) }],
523
+ }}
524
+ />
525
+ );
526
+ })}
527
+ </View>
528
+ ))}
529
+ </View>
530
+ );
531
+ }
532
+
533
+ /** Chat "is typing" bubble: three dots hop in turn inside a tinted bubble (native translate/opacity). */
534
+ function Typing({ size, color, speed, reduce }: BodyProps) {
535
+ const { radius } = useKit();
536
+ const t = useLoop(1300 / speed, linear, true, !reduce);
537
+ const bh = size * 0.62;
538
+ const dot = Math.max(2, size * 0.12);
539
+ const tail = size * 0.15;
540
+ const top = (size - bh) / 2 - size * 0.04;
541
+ const bg = alpha(color, 0.16);
542
+ return (
543
+ <View style={{ width: size, height: size }}>
544
+ <View style={{ position: 'absolute', left: 0, top: top + bh - tail * 0.25, width: tail, height: tail, borderRadius: tail / 2, backgroundColor: bg }} />
545
+ <View
546
+ style={[
547
+ styles.row,
548
+ { position: 'absolute', left: 0, right: 0, top, height: bh, gap: dot * 0.65, borderRadius: radius.capsule(bh), backgroundColor: bg },
549
+ ]}
550
+ >
551
+ {[0, 1, 2].map((i) => {
552
+ const hop = (u: number) => hump(wrap(u - i * 0.16), 0.5);
553
+ return (
554
+ <Animated.View
555
+ key={i}
556
+ style={{
557
+ width: dot,
558
+ height: dot,
559
+ borderRadius: dot / 2,
560
+ backgroundColor: color,
561
+ opacity: reduce ? 0.45 + i * 0.27 : t.interpolate(curve((u) => 0.4 + 0.6 * hop(u))),
562
+ transform: [{ translateY: reduce ? 0 : t.interpolate(curve((u) => -bh * 0.13 * hop(u))) }],
563
+ }}
564
+ />
565
+ );
566
+ })}
567
+ </View>
568
+ </View>
569
+ );
570
+ }
571
+
572
+ /** Two arcs on the outer ring spin one way while an inner arc spins the other (native rotation). */
573
+ function Arc({ size, color, speed, track, reduce }: BodyProps) {
574
+ const outer = useLoop(1150 / speed, linear, true, !reduce);
575
+ const inner = useLoop(850 / speed, linear, true, !reduce);
576
+ const stroke = Math.max(2, size / 11);
577
+ const r1 = (size - stroke) / 2;
578
+ const r2 = Math.max(stroke, r1 - stroke * 1.9);
579
+ const c1 = 2 * Math.PI * r1;
580
+ const c2 = 2 * Math.PI * r2;
581
+ const ring = (r: number, dash: string | undefined, opacity = 1) => (
582
+ <Circle cx={size / 2} cy={size / 2} r={r} stroke={color} strokeOpacity={opacity} strokeWidth={stroke} strokeLinecap="round" strokeDasharray={dash} fill="none" />
583
+ );
584
+ const spin = (v: Animated.Value, dir: 1 | -1) => v.interpolate({ inputRange: [0, 1], outputRange: ['0deg', `${360 * dir}deg`] });
585
+ return (
586
+ <View style={{ width: size, height: size }}>
587
+ {track ? (
588
+ <Svg width={size} height={size} style={StyleSheet.absoluteFill}>
589
+ {ring(r1, undefined, 0.14)}
590
+ {ring(r2, undefined, 0.14)}
591
+ </Svg>
592
+ ) : null}
593
+ <Animated.View style={[StyleSheet.absoluteFill, { transform: [{ rotate: reduce ? '30deg' : spin(outer, 1) }] }]}>
594
+ <Svg width={size} height={size}>{ring(r1, `${c1 * 0.22} ${c1 * 0.28}`)}</Svg>
595
+ </Animated.View>
596
+ <Animated.View style={[StyleSheet.absoluteFill, { transform: [{ rotate: reduce ? '-60deg' : spin(inner, -1) }] }]}>
597
+ <Svg width={size} height={size}>{ring(r2, `${c2 * 0.4} ${c2}`, 0.7)}</Svg>
598
+ </Animated.View>
599
+ </View>
600
+ );
601
+ }
602
+
603
+ /**
604
+ * A square turns a quarter at a time and rounds into a circle mid-turn: the corners fade off a
605
+ * circle underneath (native rotation/scale/opacity — no borderRadius animation).
606
+ */
607
+ function Square({ size, color, speed, reduce }: BodyProps) {
608
+ const { radius } = useKit();
609
+ const t = useLoop(1700 / speed, linear, true, !reduce);
610
+ const side = size * 0.6;
611
+ // Progress through the current quarter turn (two per loop); 1 at the very end, so the last sample is 180°.
612
+ const half = (u: number) => (u < 0.5 ? u * 2 : u * 2 - 1);
613
+ const morph = (u: number) => Math.sin(Math.PI * half(u));
614
+ const fill = { position: 'absolute' as const, width: side, height: side, backgroundColor: color };
615
+ return (
616
+ <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
617
+ <Animated.View
618
+ style={{
619
+ width: side,
620
+ height: side,
621
+ transform: reduce
622
+ ? [{ rotate: '45deg' }]
623
+ : [
624
+ { rotate: t.interpolate(curveDeg((u) => (u < 0.5 ? 0 : 90) + 90 * cubic(half(u)), 40)) },
625
+ { scale: t.interpolate(curve((u) => 1 - 0.16 * morph(u), 40)) },
626
+ ],
627
+ }}
628
+ >
629
+ <View style={[fill, { borderRadius: side / 2 }]} />
630
+ <Animated.View
631
+ style={[
632
+ fill,
633
+ {
634
+ borderRadius: radius.scale(side * 0.16),
635
+ opacity: reduce ? 0.55 : t.interpolate(curve((u) => 1 - morph(u), 40)),
636
+ },
637
+ ]}
638
+ />
639
+ </Animated.View>
640
+ </View>
641
+ );
642
+ }
643
+
644
+ /** Indeterminate progress line: a long and a short segment sweep left → right (native translateX). */
645
+ function Line({ size, color, speed, track, reduce }: BodyProps) {
646
+ const { radius } = useKit();
647
+ const h = lineThickness(size);
648
+ const [w, setW] = useState(size * 4);
649
+ const t = useLoop(1600 / speed, linear, true, !reduce);
650
+ const seg = (len: number, start: number) => {
651
+ const span = 1 - start;
652
+ return t.interpolate(
653
+ curve((u) => (u < start ? -len * w : -len * w + (1 + len) * w * cubic((u - start) / span)), 48),
654
+ );
655
+ };
656
+ const bar = { position: 'absolute' as const, left: 0, top: 0, height: h, borderRadius: radius.capsule(h), backgroundColor: color };
657
+ return (
658
+ <View
659
+ onLayout={(e) => {
660
+ const next = e.nativeEvent.layout.width;
661
+ if (next > 0) setW(next);
662
+ }}
663
+ style={{
664
+ width: '100%',
665
+ height: h,
666
+ overflow: 'hidden',
667
+ borderRadius: radius.capsule(h),
668
+ backgroundColor: track || reduce ? alpha(color, 0.16) : 'transparent',
669
+ }}
670
+ >
671
+ {reduce ? (
672
+ <View style={[bar, { left: w * 0.3, width: w * 0.4 }]} />
673
+ ) : (
674
+ <>
675
+ <Animated.View style={[bar, { width: w * 0.5, transform: [{ translateX: seg(0.5, 0) }] }]} />
676
+ <Animated.View style={[bar, { width: w * 0.22, transform: [{ translateX: seg(0.22, 0.42) }] }]} />
677
+ </>
678
+ )}
679
+ </View>
680
+ );
681
+ }
682
+
683
+ /** Label whose three trailing dots fade in one after another (native opacity). */
684
+ function Ellipsis({
685
+ text,
686
+ speed,
687
+ reduce,
688
+ color,
689
+ fontSize,
690
+ }: {
691
+ text: string;
692
+ speed: number;
693
+ reduce: boolean;
694
+ color: string;
695
+ fontSize: number;
696
+ }) {
697
+ const t = useLoop(1400 / speed, linear, true, !reduce);
698
+ const base = text.replace(/(…|\.{1,3})\s*$/, '');
699
+ return (
700
+ <View style={styles.ellipsis}>
701
+ <Text style={[styles.label, { color, fontSize }]}>{base}</Text>
702
+ {[0, 1, 2].map((i) => (
703
+ <Animated.Text
704
+ key={i}
705
+ style={{
706
+ color,
707
+ fontSize,
708
+ fontWeight: '400',
709
+ opacity: reduce
710
+ ? 1
711
+ : t.interpolate({
712
+ inputRange: [0, 0.15 + i * 0.18, 0.3 + i * 0.18, 0.85, 1],
713
+ outputRange: [0, 0, 1, 1, 0],
714
+ }),
715
+ }}
716
+ >
717
+ .
718
+ </Animated.Text>
719
+ ))}
720
+ </View>
721
+ );
722
+ }
723
+
724
+ const BODIES: Record<SpinnerVariant, (p: BodyProps) => React.JSX.Element> = {
725
+ ring: Ring,
726
+ dots: Dots,
727
+ bars: Bars,
728
+ pulse: Pulse,
729
+ orbit: Orbit,
730
+ bounce: Bounce,
731
+ wave: Wave,
732
+ grid: Grid,
733
+ typing: Typing,
734
+ arc: Arc,
735
+ square: Square,
736
+ line: Line,
737
+ };
738
+
739
+ const styles = StyleSheet.create({
740
+ root: { alignItems: 'center', justifyContent: 'center' },
741
+ row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
742
+ abs: { position: 'absolute' },
743
+ column: { flexDirection: 'column', alignItems: 'center' },
744
+ inline: { flexDirection: 'row', alignItems: 'center' },
745
+ mirror: { transform: [{ scaleX: -1 }] },
746
+ ellipsis: { flexDirection: 'row', alignItems: 'baseline', flexShrink: 1 },
747
+ label: { fontWeight: '400', flexShrink: 1 },
748
+ });