@expokit/cli 0.2.0 → 0.4.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 (232) hide show
  1. package/README.md +20 -2
  2. package/bin/expo-kit.js +219 -44
  3. package/bin/lib.mjs +110 -1
  4. package/bin/mcp.mjs +48 -19
  5. package/package.json +9 -4
  6. package/registry/_docs/accordion.json +16 -0
  7. package/registry/_docs/action-sheet.json +28 -0
  8. package/registry/_docs/alert.json +28 -0
  9. package/registry/_docs/assistant-app.json +47 -0
  10. package/registry/_docs/avatar.json +25 -1
  11. package/registry/_docs/badge.json +36 -5
  12. package/registry/_docs/bottom-nav.json +30 -6
  13. package/registry/_docs/bottom-sheet.json +28 -0
  14. package/registry/_docs/bullet.json +3 -0
  15. package/registry/_docs/button-3d.json +18 -2
  16. package/registry/_docs/button.json +16 -0
  17. package/registry/_docs/calendar.json +21 -5
  18. package/registry/_docs/card-stack.json +16 -0
  19. package/registry/_docs/card.json +20 -0
  20. package/registry/_docs/carousel.json +20 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +16 -0
  23. package/registry/_docs/checkout.json +10 -1
  24. package/registry/_docs/chip-group.json +16 -0
  25. package/registry/_docs/confetti.json +23 -3
  26. package/registry/_docs/coupon.json +16 -0
  27. package/registry/_docs/cube-pager.json +16 -0
  28. package/registry/_docs/dashboard.json +3 -0
  29. package/registry/_docs/date-picker.json +24 -4
  30. package/registry/_docs/dialog.json +24 -0
  31. package/registry/_docs/donut-shop.json +88 -0
  32. package/registry/_docs/drawer.json +32 -4
  33. package/registry/_docs/fab.json +24 -0
  34. package/registry/_docs/fintech-app.json +49 -0
  35. package/registry/_docs/flip-card.json +16 -0
  36. package/registry/_docs/floating-input.json +25 -5
  37. package/registry/_docs/food-app.json +47 -0
  38. package/registry/_docs/gift-card.json +16 -0
  39. package/registry/_docs/glass-button.json +24 -0
  40. package/registry/_docs/glass-nav-bar.json +24 -0
  41. package/registry/_docs/glass-search-bar.json +24 -0
  42. package/registry/_docs/glass-segmented.json +28 -0
  43. package/registry/_docs/glass-sheet.json +28 -0
  44. package/registry/_docs/glass-tab-bar.json +24 -0
  45. package/registry/_docs/glass.json +17 -1
  46. package/registry/_docs/grain.json +80 -0
  47. package/registry/_docs/inbox.json +3 -0
  48. package/registry/_docs/isometric-bar.json +3 -0
  49. package/registry/_docs/kit.json +3 -0
  50. package/registry/_docs/liquidglass-tab.json +3 -0
  51. package/registry/_docs/list-item.json +16 -0
  52. package/registry/_docs/loyalty-card.json +16 -0
  53. package/registry/_docs/morphing-button-group.json +96 -0
  54. package/registry/_docs/number-ticker.json +16 -0
  55. package/registry/_docs/offers.json +3 -0
  56. package/registry/_docs/onboarding.json +3 -0
  57. package/registry/_docs/orb-reveal.json +132 -0
  58. package/registry/_docs/otp-input.json +28 -4
  59. package/registry/_docs/parallax-layers.json +20 -0
  60. package/registry/_docs/parallax-scroll.json +24 -0
  61. package/registry/_docs/password-input.json +23 -3
  62. package/registry/_docs/payment-card.json +16 -0
  63. package/registry/_docs/pixel-bar.json +3 -0
  64. package/registry/_docs/polar-area.json +3 -0
  65. package/registry/_docs/profile.json +3 -0
  66. package/registry/_docs/progress-steps.json +24 -0
  67. package/registry/_docs/progress.json +20 -0
  68. package/registry/_docs/qr-code-button.json +122 -0
  69. package/registry/_docs/radio-group.json +20 -0
  70. package/registry/_docs/rating.json +24 -0
  71. package/registry/_docs/ring-carousel.json +20 -0
  72. package/registry/_docs/scratch-card.json +20 -0
  73. package/registry/_docs/search-bar.json +23 -3
  74. package/registry/_docs/search-results.json +3 -0
  75. package/registry/_docs/segmented-control.json +20 -0
  76. package/registry/_docs/select.json +33 -8
  77. package/registry/_docs/settings.json +9 -0
  78. package/registry/_docs/sign-in.json +3 -0
  79. package/registry/_docs/skeleton.json +22 -2
  80. package/registry/_docs/slide-to-confirm.json +24 -0
  81. package/registry/_docs/slider.json +27 -7
  82. package/registry/_docs/spin-wheel.json +24 -0
  83. package/registry/_docs/spinner.json +41 -11
  84. package/registry/_docs/stateful-button.json +184 -0
  85. package/registry/_docs/stepper.json +23 -7
  86. package/registry/_docs/swipe-deck.json +16 -0
  87. package/registry/_docs/swipeable-row.json +24 -1
  88. package/registry/_docs/switch.json +20 -0
  89. package/registry/_docs/tabs.json +24 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +18 -2
  92. package/registry/_docs/ticket-card.json +20 -0
  93. package/registry/_docs/ticket-shape.json +4 -1
  94. package/registry/_docs/tilt-card.json +20 -4
  95. package/registry/_docs/time-picker.json +22 -6
  96. package/registry/_docs/timeline-chart.json +3 -0
  97. package/registry/_docs/timeline.json +18 -2
  98. package/registry/_docs/toast.json +32 -8
  99. package/registry/_docs/treemap.json +3 -0
  100. package/registry/_docs/waterfall.json +3 -0
  101. package/registry/_docs/wheel-list.json +20 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
  103. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  104. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
  105. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  106. package/registry/alert/files/src/components/ui/alert.tsx +7 -4
  107. package/registry/alert/files/src/components/ui/button.tsx +4 -3
  108. package/registry/alert/files/src/lib/kit.tsx +8 -5
  109. package/registry/assistant-app.json +49 -0
  110. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  111. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  112. package/registry/badge/files/src/components/ui/badge.tsx +345 -21
  113. package/registry/badge/files/src/lib/kit.tsx +8 -5
  114. package/registry/badge.json +1 -1
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  117. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  118. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  119. package/registry/button/files/src/components/ui/button.tsx +4 -3
  120. package/registry/button/files/src/lib/kit.tsx +8 -5
  121. package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
  122. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  123. package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
  124. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  125. package/registry/card/files/src/components/ui/card.tsx +8 -6
  126. package/registry/card/files/src/lib/kit.tsx +8 -5
  127. package/registry/chat.json +18 -0
  128. package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
  129. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  130. package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
  131. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  132. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  133. package/registry/coupon/files/src/components/ui/coupon.tsx +23 -20
  134. package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
  135. package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
  136. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  137. package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
  138. package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
  139. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  140. package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
  141. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  142. package/registry/donut-shop.json +18 -0
  143. package/registry/drawer/files/src/components/ui/drawer.tsx +25 -19
  144. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  145. package/registry/fintech-app.json +57 -0
  146. package/registry/flip-card/files/src/components/ui/flip-card.tsx +1 -1
  147. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  148. package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
  149. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  150. package/registry/food-app.json +55 -0
  151. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  152. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  153. package/registry/grain.json +17 -0
  154. package/registry/kit/files/src/lib/kit.tsx +8 -5
  155. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
  156. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  157. package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
  158. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  159. package/registry/morphing-button-group.json +18 -0
  160. package/registry/orb-reveal.json +20 -0
  161. package/registry/otp-input/files/src/components/ui/otp-input.tsx +8 -6
  162. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  163. package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
  164. package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
  165. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  166. package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
  167. package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
  168. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
  169. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  170. package/registry/progress/files/src/components/ui/progress.tsx +8 -5
  171. package/registry/progress/files/src/lib/kit.tsx +8 -5
  172. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -11
  173. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  174. package/registry/qr-code-button.json +18 -0
  175. package/registry/radio-group/files/src/components/ui/radio-group.tsx +10 -9
  176. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  177. package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
  178. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  179. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
  180. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  181. package/registry/select/files/src/components/ui/select.tsx +3 -3
  182. package/registry/select/files/src/lib/kit.tsx +8 -5
  183. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  184. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  185. package/registry/slider/files/src/components/ui/slider.tsx +5 -4
  186. package/registry/slider/files/src/lib/kit.tsx +8 -5
  187. package/registry/spinner/files/src/components/ui/spinner.tsx +452 -34
  188. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  189. package/registry/spinner.json +1 -1
  190. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  191. package/registry/{pagination-dots → stateful-button}/files/src/lib/kit.tsx +8 -5
  192. package/registry/stateful-button.json +18 -0
  193. package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
  194. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  195. package/registry/switch/files/src/components/ui/switch.tsx +4 -3
  196. package/registry/switch/files/src/lib/kit.tsx +8 -5
  197. package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
  198. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  199. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  200. package/registry/text-animation/files/src/lib/kit.tsx +222 -0
  201. package/registry/text-animation.json +15 -0
  202. package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
  203. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  204. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  205. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +2 -2
  206. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  207. package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
  208. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  209. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  210. package/registry/timeline/files/src/components/ui/timeline.tsx +12 -10
  211. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  212. package/registry/toast/files/src/components/ui/button.tsx +4 -3
  213. package/registry/toast/files/src/components/ui/toast.tsx +13 -11
  214. package/registry/toast/files/src/lib/kit.tsx +8 -5
  215. package/schema/components.schema.json +1 -1
  216. package/registry/_docs/color-swatch.json +0 -66
  217. package/registry/_docs/kpi-card.json +0 -11
  218. package/registry/_docs/like-button.json +0 -72
  219. package/registry/_docs/morph-button.json +0 -108
  220. package/registry/_docs/pagination-dots.json +0 -88
  221. package/registry/_docs/shimmer-button.json +0 -78
  222. package/registry/_docs/tag-input.json +0 -128
  223. package/registry/color-swatch.json +0 -17
  224. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  225. package/registry/kpi-card.json +0 -14
  226. package/registry/like-button.json +0 -18
  227. package/registry/morph-button.json +0 -18
  228. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  229. package/registry/pagination-dots.json +0 -15
  230. package/registry/shimmer-button.json +0 -17
  231. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  232. package/registry/tag-input.json +0 -17
@@ -15,7 +15,19 @@ import { alpha, useKit } from '@/lib/kit';
15
15
 
16
16
  const AnimatedCircle = Animated.createAnimatedComponent(Circle);
17
17
 
18
- export type SpinnerVariant = 'ring' | 'dots';
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';
19
31
  export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl';
20
32
  export type SpinnerLabelPosition = 'none' | 'right' | 'bottom';
21
33
 
@@ -31,12 +43,21 @@ export interface SpinnerProps {
31
43
  label?: string;
32
44
  /** Show `label` beside or under the spinner with an animated ellipsis (default 'none': screen readers only). */
33
45
  labelPosition?: SpinnerLabelPosition;
34
- /** Faint full circle behind the ring. */
46
+ /** Faint guide behind the motion: the full circle (ring, orbit, arc), the rail (line) or ghost bars (bars). */
35
47
  track?: boolean;
36
48
  style?: StyleProp<ViewStyle>;
37
49
  }
38
50
 
39
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
+ }
40
61
  /** Arcs stacked from the head backwards: the shortest is solid, the rest faint, so the tail fades out. */
41
62
  const TAIL = 7;
42
63
 
@@ -99,8 +120,36 @@ function eased(points: number[], values: number[], ease: (t: number) => number,
99
120
  }
100
121
 
101
122
  /**
102
- * Loading indicator — a comet arc whose tail fades out and breathes as it spins, or a soft wave of
103
- * dots. Optionally shows a label with an animated ellipsis. Rotation runs on the native driver.
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.
104
153
  */
105
154
  export function Spinner({
106
155
  variant = 'ring',
@@ -119,6 +168,7 @@ export function Spinner({
119
168
  const Body = BODIES[variant] ?? Ring;
120
169
  const s = Math.max(0.1, speed);
121
170
  const body = <Body size={px} color={tint} speed={s} track={track} reduce={reduce} />;
171
+ const box = boxOf(BODIES[variant] ? variant : 'ring', px);
122
172
 
123
173
  if (labelPosition === 'none' || !label) {
124
174
  return (
@@ -127,7 +177,7 @@ export function Spinner({
127
177
  accessibilityRole="progressbar"
128
178
  accessibilityLabel={label}
129
179
  accessibilityState={{ busy: true }}
130
- style={[styles.root, { width: px, height: px }, style]}
180
+ style={[styles.root, box, style]}
131
181
  >
132
182
  {body}
133
183
  </View>
@@ -143,7 +193,7 @@ export function Spinner({
143
193
  accessibilityState={{ busy: true }}
144
194
  style={[bottom ? styles.column : styles.inline, { gap: bottom ? 10 : 8 }, style]}
145
195
  >
146
- <View style={[styles.root, { width: px, height: px }]}>{body}</View>
196
+ <View style={[styles.root, box]}>{body}</View>
147
197
  <Ellipsis
148
198
  text={label}
149
199
  speed={s}
@@ -236,10 +286,8 @@ function Dots({ size, color, speed, reduce }: BodyProps) {
236
286
  {[0, 1, 2].map((i) => {
237
287
  // A smooth wave: each dot swells, brightens and lifts slightly during its slice of the cycle.
238
288
  const s = i * 0.16;
239
- const range = {
240
- inputRange: [0, s, s + 0.22, s + 0.44, 1],
241
- easing: sine,
242
- };
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);
243
291
  return (
244
292
  <Animated.View
245
293
  key={i}
@@ -250,24 +298,15 @@ function Dots({ size, color, speed, reduce }: BodyProps) {
250
298
  backgroundColor: color,
251
299
  opacity: reduce
252
300
  ? 0.4 + i * 0.3
253
- : t.interpolate({
254
- ...range,
255
- outputRange: [0.35, 0.35, 1, 0.35, 0.35],
256
- }),
301
+ : t.interpolate(wave([0.35, 0.35, 1, 0.35, 0.35])),
257
302
  transform: reduce
258
303
  ? []
259
304
  : [
260
305
  {
261
- translateY: t.interpolate({
262
- ...range,
263
- outputRange: [0, 0, -size / 9, 0, 0],
264
- }),
306
+ translateY: t.interpolate(wave([0, 0, -size / 9, 0, 0])),
265
307
  },
266
308
  {
267
- scale: t.interpolate({
268
- ...range,
269
- outputRange: [0.65, 0.65, 1.1, 0.65, 0.65],
270
- }),
309
+ scale: t.interpolate(wave([0.65, 0.65, 1.1, 0.65, 0.65])),
271
310
  },
272
311
  ],
273
312
  }}
@@ -278,6 +317,369 @@ function Dots({ size, color, speed, reduce }: BodyProps) {
278
317
  );
279
318
  }
280
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
+
281
683
  /** Label whose three trailing dots fade in one after another (native opacity). */
282
684
  function Ellipsis({
283
685
  text,
@@ -292,25 +694,30 @@ function Ellipsis({
292
694
  color: string;
293
695
  fontSize: number;
294
696
  }) {
697
+ const { font } = useKit();
698
+ const ff = font.family ? { fontFamily: font.family } : null;
295
699
  const t = useLoop(1400 / speed, linear, true, !reduce);
296
700
  const base = text.replace(/(…|\.{1,3})\s*$/, '');
297
701
  return (
298
702
  <View style={styles.ellipsis}>
299
- <Text style={[styles.label, { color, fontSize }]}>{base}</Text>
703
+ <Text style={[styles.label, ff, { color, fontSize }]}>{base}</Text>
300
704
  {[0, 1, 2].map((i) => (
301
705
  <Animated.Text
302
706
  key={i}
303
- style={{
304
- color,
305
- fontSize,
306
- fontWeight: '400',
307
- opacity: reduce
308
- ? 1
309
- : t.interpolate({
310
- inputRange: [0, 0.15 + i * 0.18, 0.3 + i * 0.18, 0.85, 1],
311
- outputRange: [0, 0, 1, 1, 0],
312
- }),
313
- }}
707
+ style={[
708
+ ff,
709
+ {
710
+ color,
711
+ fontSize,
712
+ fontWeight: '400',
713
+ opacity: reduce
714
+ ? 1
715
+ : t.interpolate({
716
+ inputRange: [0, 0.15 + i * 0.18, 0.3 + i * 0.18, 0.85, 1],
717
+ outputRange: [0, 0, 1, 1, 0],
718
+ }),
719
+ },
720
+ ]}
314
721
  >
315
722
  .
316
723
  </Animated.Text>
@@ -322,11 +729,22 @@ function Ellipsis({
322
729
  const BODIES: Record<SpinnerVariant, (p: BodyProps) => React.JSX.Element> = {
323
730
  ring: Ring,
324
731
  dots: Dots,
732
+ bars: Bars,
733
+ pulse: Pulse,
734
+ orbit: Orbit,
735
+ bounce: Bounce,
736
+ wave: Wave,
737
+ grid: Grid,
738
+ typing: Typing,
739
+ arc: Arc,
740
+ square: Square,
741
+ line: Line,
325
742
  };
326
743
 
327
744
  const styles = StyleSheet.create({
328
745
  root: { alignItems: 'center', justifyContent: 'center' },
329
746
  row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
747
+ abs: { position: 'absolute' },
330
748
  column: { flexDirection: 'column', alignItems: 'center' },
331
749
  inline: { flexDirection: 'row', alignItems: 'center' },
332
750
  mirror: { transform: [{ scaleX: -1 }] },
@@ -181,14 +181,17 @@ export function useKit(): Theme {
181
181
  return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
182
  }
183
183
 
184
- /** Shared motion presets so the whole kit moves the same way. */
184
+ /**
185
+ * Shared motion presets so the whole kit moves the same way. Every spring is critically damped
186
+ * (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
187
+ */
185
188
  export const motion = {
186
189
  /** Toggles, selection indicators. */
187
- snappy: { damping: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
190
+ snappy: { damping: 30, stiffness: 260, mass: 0.8 },
191
+ /** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
192
+ bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
190
193
  /** Sheets, layout changes. */
191
- gentle: { damping: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };
@@ -2,7 +2,7 @@
2
2
  "name": "spinner",
3
3
  "type": "component",
4
4
  "title": "Spinner",
5
- "description": "A comet arc whose tail fades out and breathes as it spins (native rotation), or a soft staggered wave of dots. Optional faint track and a visible label with an animated ellipsis.",
5
+ "description": "A loader pack: the comet ring whose tail breathes as it spins, a staggered dot wave, and ten more — equalizer bars, pulse ripple, orbit, squash-bounce, sine wave, 3×3 grid, typing bubble, counter-rotating arcs, morphing square and an indeterminate line. All native-driver loops, one size/colour/speed API, optional track and an animated-ellipsis label.",
6
6
  "tier": "free",
7
7
  "dependencies": [
8
8
  "react-native-svg"