@expokit/cli 0.3.0 → 0.4.1

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 (188) hide show
  1. package/README.md +8 -4
  2. package/bin/expo-kit.js +137 -24
  3. package/bin/lib.mjs +110 -1
  4. package/bin/mcp.mjs +40 -18
  5. package/package.json +2 -2
  6. package/registry/_docs/accordion.json +1 -1
  7. package/registry/_docs/assistant-app.json +47 -0
  8. package/registry/_docs/avatar.json +6 -6
  9. package/registry/_docs/badge.json +4 -4
  10. package/registry/_docs/bottom-nav.json +6 -6
  11. package/registry/_docs/bottom-sheet.json +3 -3
  12. package/registry/_docs/bullet.json +6 -0
  13. package/registry/_docs/button-3d.json +3 -3
  14. package/registry/_docs/button.json +2 -2
  15. package/registry/_docs/calendar.json +5 -5
  16. package/registry/_docs/card.json +1 -1
  17. package/registry/_docs/checkout.json +7 -1
  18. package/registry/_docs/chip-group.json +1 -1
  19. package/registry/_docs/confetti.json +3 -3
  20. package/registry/_docs/date-picker.json +4 -4
  21. package/registry/_docs/donut-shop.json +88 -0
  22. package/registry/_docs/drawer.json +4 -4
  23. package/registry/_docs/fintech-app.json +49 -0
  24. package/registry/_docs/flip-card.json +4 -4
  25. package/registry/_docs/floating-input.json +5 -5
  26. package/registry/_docs/food-app.json +47 -0
  27. package/registry/_docs/glass.json +1 -1
  28. package/registry/_docs/grain.json +80 -0
  29. package/registry/_docs/isometric-bar.json +6 -0
  30. package/registry/_docs/liquidglass-tab.json +1 -1
  31. package/registry/_docs/morphing-button-group.json +96 -0
  32. package/registry/_docs/orb-reveal.json +132 -0
  33. package/registry/_docs/otp-input.json +4 -4
  34. package/registry/_docs/password-input.json +3 -3
  35. package/registry/_docs/payment-card.json +3 -3
  36. package/registry/_docs/pixel-bar.json +6 -0
  37. package/registry/_docs/polar-area.json +6 -0
  38. package/registry/_docs/progress-steps.json +4 -4
  39. package/registry/_docs/progress.json +2 -2
  40. package/registry/_docs/qr-code-button.json +122 -0
  41. package/registry/_docs/radio-group.json +1 -1
  42. package/registry/_docs/search-bar.json +4 -4
  43. package/registry/_docs/segmented-control.json +1 -1
  44. package/registry/_docs/select.json +7 -7
  45. package/registry/_docs/settings.json +6 -0
  46. package/registry/_docs/skeleton.json +3 -3
  47. package/registry/_docs/slider.json +6 -6
  48. package/registry/_docs/spinner.json +3 -3
  49. package/registry/_docs/stateful-button.json +1 -1
  50. package/registry/_docs/stepper.json +7 -7
  51. package/registry/_docs/tabs.json +2 -2
  52. package/registry/_docs/text-animation.json +4 -4
  53. package/registry/_docs/textarea.json +3 -3
  54. package/registry/_docs/ticket-shape.json +1 -1
  55. package/registry/_docs/tilt-card.json +4 -4
  56. package/registry/_docs/time-picker.json +6 -6
  57. package/registry/_docs/timeline-chart.json +6 -0
  58. package/registry/_docs/timeline.json +6 -6
  59. package/registry/_docs/toast.json +8 -8
  60. package/registry/_docs/treemap.json +6 -0
  61. package/registry/_docs/waterfall.json +6 -0
  62. package/registry/accordion/files/src/components/ui/accordion.tsx +36 -7
  63. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  64. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +12 -8
  65. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  66. package/registry/alert/files/src/components/ui/alert.tsx +11 -5
  67. package/registry/alert/files/src/components/ui/button.tsx +35 -6
  68. package/registry/alert/files/src/lib/kit.tsx +8 -5
  69. package/registry/assistant-app.json +49 -0
  70. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  71. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  72. package/registry/badge/files/src/components/ui/badge.tsx +23 -9
  73. package/registry/badge/files/src/lib/kit.tsx +8 -5
  74. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +21 -8
  75. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  76. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  77. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  78. package/registry/button/files/src/components/ui/button.tsx +35 -6
  79. package/registry/button/files/src/lib/kit.tsx +8 -5
  80. package/registry/button-3d/files/src/components/ui/button-3d.tsx +35 -7
  81. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  82. package/registry/calendar/files/src/components/ui/calendar.tsx +68 -19
  83. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  84. package/registry/card/files/src/components/ui/card.tsx +32 -10
  85. package/registry/card/files/src/lib/kit.tsx +8 -5
  86. package/registry/checkbox/files/src/components/ui/checkbox.tsx +31 -8
  87. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  88. package/registry/chip-group/files/src/components/ui/chip-group.tsx +39 -7
  89. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  90. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  91. package/registry/coupon/files/src/components/ui/coupon.tsx +55 -28
  92. package/registry/coupon/files/src/components/ui/flip-card.tsx +23 -4
  93. package/registry/coupon/files/src/components/ui/tilt-card.tsx +59 -18
  94. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  95. package/registry/date-picker/files/src/components/ui/calendar.tsx +68 -19
  96. package/registry/date-picker/files/src/components/ui/date-picker.tsx +98 -44
  97. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  98. package/registry/dialog/files/src/components/ui/dialog.tsx +19 -9
  99. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  100. package/registry/donut-shop.json +18 -0
  101. package/registry/drawer/files/src/components/ui/drawer.tsx +31 -21
  102. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  103. package/registry/fintech-app.json +57 -0
  104. package/registry/flip-card/files/src/components/ui/flip-card.tsx +23 -4
  105. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  106. package/registry/floating-input/files/src/components/ui/floating-input.tsx +91 -32
  107. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  108. package/registry/food-app.json +55 -0
  109. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  110. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  111. package/registry/grain.json +17 -0
  112. package/registry/kit/files/src/lib/kit.tsx +8 -5
  113. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -2
  114. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  115. package/registry/list-item/files/src/components/ui/list-item.tsx +28 -17
  116. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  117. package/registry/morphing-button-group.json +18 -0
  118. package/registry/orb-reveal.json +20 -0
  119. package/registry/otp-input/files/src/components/ui/otp-input.tsx +57 -24
  120. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  121. package/registry/password-input/files/src/components/ui/floating-input.tsx +91 -32
  122. package/registry/password-input/files/src/components/ui/password-input.tsx +55 -21
  123. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  124. package/registry/payment-card/files/src/components/ui/flip-card.tsx +23 -4
  125. package/registry/payment-card/files/src/components/ui/payment-card.tsx +35 -18
  126. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +59 -18
  127. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  128. package/registry/progress/files/src/components/ui/progress.tsx +16 -6
  129. package/registry/progress/files/src/lib/kit.tsx +8 -5
  130. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +22 -11
  131. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  132. package/registry/qr-code-button.json +18 -0
  133. package/registry/radio-group/files/src/components/ui/radio-group.tsx +48 -17
  134. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  135. package/registry/search-bar/files/src/components/ui/search-bar.tsx +81 -26
  136. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  137. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +40 -10
  138. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  139. package/registry/select/files/src/components/ui/select.tsx +161 -86
  140. package/registry/select/files/src/lib/kit.tsx +8 -5
  141. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  142. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  143. package/registry/slider/files/src/components/ui/slider.tsx +56 -27
  144. package/registry/slider/files/src/lib/kit.tsx +8 -5
  145. package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
  146. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  147. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
  148. package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
  149. package/registry/stepper/files/src/components/ui/stepper.tsx +83 -29
  150. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  151. package/registry/switch/files/src/components/ui/switch.tsx +60 -22
  152. package/registry/switch/files/src/lib/kit.tsx +8 -5
  153. package/registry/tabs/files/src/components/ui/tabs.tsx +15 -10
  154. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  155. package/registry/text-animation/files/src/components/ui/text-animation.tsx +11 -6
  156. package/registry/text-animation/files/src/lib/kit.tsx +8 -5
  157. package/registry/textarea/files/src/components/ui/textarea.tsx +115 -45
  158. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  159. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  160. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +59 -18
  161. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  162. package/registry/time-picker/files/src/components/ui/time-picker.tsx +156 -75
  163. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  164. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  165. package/registry/timeline/files/src/components/ui/timeline.tsx +19 -12
  166. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  167. package/registry/toast/files/src/components/ui/button.tsx +35 -6
  168. package/registry/toast/files/src/components/ui/toast.tsx +25 -14
  169. package/registry/toast/files/src/lib/kit.tsx +8 -5
  170. package/schema/components.schema.json +1 -1
  171. package/registry/_docs/color-swatch.json +0 -82
  172. package/registry/_docs/kpi-card.json +0 -14
  173. package/registry/_docs/like-button.json +0 -92
  174. package/registry/_docs/morph-button.json +0 -128
  175. package/registry/_docs/pagination-dots.json +0 -108
  176. package/registry/_docs/shimmer-button.json +0 -98
  177. package/registry/_docs/tag-input.json +0 -148
  178. package/registry/color-swatch.json +0 -17
  179. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  180. package/registry/kpi-card.json +0 -14
  181. package/registry/like-button.json +0 -18
  182. package/registry/morph-button.json +0 -18
  183. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  184. package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
  185. package/registry/pagination-dots.json +0 -15
  186. package/registry/shimmer-button.json +0 -17
  187. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  188. package/registry/tag-input.json +0 -17
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
1
+ import React, { createContext, useContext, useEffect, useId, useMemo, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
3
  AccessibilityInfo,
4
4
  Animated,
@@ -131,7 +131,7 @@ interface Frame {
131
131
 
132
132
  const GroupContext = createContext<{ frame: Frame | null } | null>(null);
133
133
 
134
- function measure(node: View | null, e: LayoutChangeEvent, done: (f: Frame) => void) {
134
+ function measure(node: ComponentRef<typeof View> | null, e: LayoutChangeEvent, done: (f: Frame) => void) {
135
135
  const { x, y, width, height } = e.nativeEvent.layout;
136
136
  const fallback = () => done({ x, y, w: width, h: height });
137
137
  if (!node || typeof node.measureInWindow !== 'function') return fallback();
@@ -153,7 +153,7 @@ export interface SkeletonGroupProps {
153
153
  */
154
154
  export function SkeletonGroup({ children, style }: SkeletonGroupProps) {
155
155
  const parent = useContext(GroupContext);
156
- const ref = useRef<View>(null);
156
+ const ref = useRef<ComponentRef<typeof View>>(null);
157
157
  const [frame, setFrame] = useState<Frame | null>(null);
158
158
  const value = useMemo(() => ({ frame }), [frame]);
159
159
  if (parent) return <View style={style}>{children}</View>;
@@ -182,7 +182,7 @@ export function Skeleton({ width = '100%', height = 14, radius = 'sm', variant =
182
182
  const t = useClock(moving);
183
183
  const group = useContext(GroupContext);
184
184
  const win = useWindowDimensions();
185
- const ref = useRef<View>(null);
185
+ const ref = useRef<ComponentRef<typeof View>>(null);
186
186
  const [box, setBox] = useState<Frame | null>(null);
187
187
  const gid = 'sk' + useId().replace(/[^a-zA-Z0-9]/g, '');
188
188
 
@@ -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
  };
@@ -1,11 +1,14 @@
1
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  Animated,
5
+ Easing,
4
6
  PanResponder,
5
7
  Pressable,
6
8
  StyleSheet,
7
9
  Text,
8
10
  View,
11
+ useWindowDimensions,
9
12
  type AccessibilityActionEvent,
10
13
  type GestureResponderEvent,
11
14
  type StyleProp,
@@ -110,13 +113,22 @@ const SIZES: Record<SliderSize, { track: number; stepped: number; thumb: number;
110
113
  md: { track: 4, stepped: 8, thumb: 26, bar: 46, icon: 20, font: 13, hit: 44 },
111
114
  lg: { track: 6, stepped: 10, thumb: 30, bar: 58, icon: 24, font: 15, hit: 52 },
112
115
  };
116
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
117
+ // content text scales all the way.
118
+ const FONT_CAP = 1.35;
113
119
  const BUBBLE_H = 28;
114
120
  const BUBBLE_GAP = 10;
115
121
  const BUBBLE_W = 120;
116
122
  /** Below this many points per step the thumb follows the finger and snaps on release. */
117
123
  const SNAP_PX = 14;
124
+ const SMOOTH = Easing.bezier(0.32, 0.72, 0, 1);
118
125
 
119
126
  const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
127
+ /** Native-driven move to `toValue`: a snappy spring, or a short ease when Reduce Motion is on. */
128
+ const glide = (v: Animated.Value, toValue: number, reduce: boolean) =>
129
+ reduce
130
+ ? Animated.timing(v, { toValue, duration: 150, easing: SMOOTH, useNativeDriver: true })
131
+ : Animated.spring(v, { toValue, useNativeDriver: true, ...motion.snappy });
120
132
  const decimals = (n: number) => (String(n).split('.')[1] ?? '').length;
121
133
 
122
134
  /**
@@ -161,10 +173,14 @@ export function Slider(props: SliderProps) {
161
173
  const onSlidingStart = props.onSlidingStart as Callback;
162
174
  const onSlidingComplete = props.onSlidingComplete as Callback;
163
175
 
164
- const { colors, radius, dark } = useKit();
176
+ const { colors, radius, dark, font } = useKit();
177
+ const ff = font.family ? { fontFamily: font.family } : null;
178
+ const reduce = useReduceMotion();
165
179
  const tint = accent ?? colors.accent;
166
180
  const onTint = accent ? readableOn(accent) : colors.accentForeground;
167
181
  const sz = SIZES[size];
182
+ // Text scale the capped labels actually render at, for the label row's height.
183
+ const fontScale = Math.min(useWindowDimensions().fontScale, FONT_CAP);
168
184
 
169
185
  const T = isBar ? 0 : sz.thumb; // thumb diameter
170
186
  const inset = T / 2; // thumb centre travels [inset, w - inset]
@@ -210,29 +226,24 @@ export function Slider(props: SliderProps) {
210
226
  const held = useRef(new Animated.Value(0)).current;
211
227
 
212
228
  // Everything the (once-created) PanResponder reads, kept fresh every render.
213
- const live = useRef({ vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete });
214
- live.current = { vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete };
229
+ const live = useRef({ vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, reduce, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete });
230
+ live.current = { vals, w, inset, step, span, min, range, isBar, T, disabled, controlled, reduce, snap, ratioOf, fmt, out, onValueChange, onSlidingStart, onSlidingComplete };
215
231
  const drag = useRef({ dragging: false, thumb: 0, startX: 0, origin: 0, relative: false, moved: false, cur: [] as number[], target: [-1, -1] });
216
232
 
217
233
  // Follow value changes from outside (controlled updates, a11y, icon taps) when nobody is dragging.
218
234
  const key = vals.join(',');
219
235
  useEffect(() => {
220
236
  if (drag.current.dragging) return;
221
- vals.forEach((v, i) => Animated.spring(ratios[i], { toValue: ratioOf(v), useNativeDriver: true, ...motion.snappy }).start());
237
+ vals.forEach((v, i) => glide(ratios[i], ratioOf(v), reduce).start());
222
238
  // eslint-disable-next-line react-hooks/exhaustive-deps
223
239
  }, [key, min, max]);
224
240
 
241
+ // With Reduce Motion these only fade (the scale / lift outputs below are flattened).
225
242
  useEffect(() => {
226
- press.forEach((p, i) => Animated.spring(p, { toValue: active === i ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start());
227
- bubble.forEach((b, i) =>
228
- Animated.spring(b, {
229
- toValue: bubbleShown || (showValue === 'drag' && active === i) ? 1 : 0,
230
- useNativeDriver: true,
231
- ...motion.snappy,
232
- }).start()
233
- );
234
- Animated.spring(held, { toValue: active !== null ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
235
- }, [active, bubbleShown, showValue, press, bubble, held]);
243
+ press.forEach((p, i) => glide(p, active === i ? 1 : 0, reduce).start());
244
+ bubble.forEach((b, i) => glide(b, bubbleShown || (showValue === 'drag' && active === i) ? 1 : 0, reduce).start());
245
+ glide(held, active !== null ? 1 : 0, reduce).start();
246
+ }, [active, bubbleShown, showValue, press, bubble, held, reduce]);
236
247
 
237
248
  const pan = useMemo(() => {
238
249
  const usableOf = () => Math.max(1, live.current.w - 2 * live.current.inset);
@@ -248,7 +259,7 @@ export function Slider(props: SliderProps) {
248
259
  const coarse = !!L.step && (L.step / L.span) * usableOf() >= SNAP_PX;
249
260
  const target = coarse ? L.ratioOf(snapped) : L.ratioOf(v);
250
261
  if (animate || coarse) {
251
- if (target !== d.target[i]) Animated.spring(ratios[i], { toValue: target, useNativeDriver: true, ...motion.snappy }).start();
262
+ if (target !== d.target[i]) glide(ratios[i], target, L.reduce).start();
252
263
  } else {
253
264
  ratios[i].setValue(target);
254
265
  }
@@ -268,7 +279,7 @@ export function Slider(props: SliderProps) {
268
279
  const d = drag.current;
269
280
  if (!d.dragging) return;
270
281
  if (seek) moveTo(d.startX, true);
271
- Animated.spring(ratios[d.thumb], { toValue: L.ratioOf(d.cur[d.thumb]), useNativeDriver: true, ...motion.snappy }).start();
282
+ glide(ratios[d.thumb], L.ratioOf(d.cur[d.thumb]), L.reduce).start();
272
283
  d.dragging = false;
273
284
  d.target = [-1, -1];
274
285
  if (!L.controlled) setInner(d.cur);
@@ -349,17 +360,18 @@ export function Slider(props: SliderProps) {
349
360
  };
350
361
  }, [w, inset, usable, range, slotW, ratios]);
351
362
 
363
+ // Reduce Motion: no swell, no pop-up — the halo and tooltip just fade.
352
364
  const anim = useMemo(
353
365
  () => ({
354
- thumb: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, 1.14] }) }]),
355
- halo: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, 1.75] }) }]),
366
+ thumb: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, reduce ? 1 : 1.14] }) }]),
367
+ halo: press.map((p) => [{ scale: p.interpolate({ inputRange: [0, 1], outputRange: [1, reduce ? 1 : 1.75] }) }]),
356
368
  bubble: bubble.map((b) => [
357
- { translateY: b.interpolate({ inputRange: [0, 1], outputRange: [6, 0] }) },
358
- { scale: b.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] }) },
369
+ { translateY: b.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 6, 0] }) },
370
+ { scale: b.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.6, 1] }) },
359
371
  ]),
360
- held: [{ scale: held.interpolate({ inputRange: [0, 1], outputRange: [1, 1.025] }) }],
372
+ held: [{ scale: held.interpolate({ inputRange: [0, 1], outputRange: [1, reduce ? 1 : 1.025] }) }],
361
373
  }),
362
- [press, bubble, held]
374
+ [press, bubble, held, reduce]
363
375
  );
364
376
 
365
377
  // ---- marks ----------------------------------------------------------------------------------
@@ -412,7 +424,7 @@ export function Slider(props: SliderProps) {
412
424
  <View style={[styles.barContent, { width: w, height: trackH, paddingHorizontal: Math.round(trackH * 0.32) }]}>
413
425
  <View>{icon !== undefined ? drawIcon(icon, iconColor) : null}</View>
414
426
  {showValue === 'always' ? (
415
- <Text numberOfLines={1} selectable={false} style={[styles.barValue, { color: textColor, fontSize: sz.font + 1 }, labelStyle]}>
427
+ <Text numberOfLines={1} selectable={false} maxFontSizeMultiplier={FONT_CAP} style={[styles.barValue, ff, { color: textColor, fontSize: sz.font + 1 }, labelStyle]}>
416
428
  {range ? `${fmt(vals[0])} – ${fmt(vals[1])}` : fmt(vals[0])}
417
429
  </Text>
418
430
  ) : null}
@@ -424,7 +436,8 @@ export function Slider(props: SliderProps) {
424
436
  key={key}
425
437
  numberOfLines={1}
426
438
  selectable={false}
427
- style={[styles.markLabel, place, { fontSize: sz.font - 1, color: lit ? colors.foreground : colors.mutedForeground }, labelStyle]}
439
+ maxFontSizeMultiplier={FONT_CAP}
440
+ style={[styles.markLabel, place, ff, { fontSize: sz.font - 1, color: lit ? colors.foreground : colors.mutedForeground }, labelStyle]}
428
441
  >
429
442
  {text}
430
443
  </Text>
@@ -569,7 +582,7 @@ export function Slider(props: SliderProps) {
569
582
  ]}
570
583
  >
571
584
  <View style={[styles.bubble, { backgroundColor: colors.foreground, borderRadius: radius.capsule(BUBBLE_H) }]}>
572
- <Text numberOfLines={1} selectable={false} style={[styles.bubbleText, { color: colors.background, fontSize: sz.font }, labelStyle]}>
585
+ <Text numberOfLines={1} selectable={false} maxFontSizeMultiplier={FONT_CAP} style={[styles.bubbleText, ff, { color: colors.background, fontSize: sz.font }, labelStyle]}>
573
586
  {fmt(v)}
574
587
  </Text>
575
588
  </View>
@@ -582,7 +595,7 @@ export function Slider(props: SliderProps) {
582
595
  </View>
583
596
 
584
597
  {hasMarkLabels || minMax ? (
585
- <View pointerEvents="none" style={[styles.labels, { height: sz.font + 8 }]}>
598
+ <View pointerEvents="none" style={[styles.labels, { height: Math.round(sz.font * fontScale) + 8 }]}>
586
599
  {hasMarkLabels
587
600
  ? markList.map((m) => {
588
601
  if (!m.label || !w) return null;
@@ -671,6 +684,22 @@ export function SliderIcon({ name, size = 20, color }: { name: SliderIconName; s
671
684
  );
672
685
  }
673
686
 
687
+ function useReduceMotion() {
688
+ const [reduce, setReduce] = useState(false);
689
+ useEffect(() => {
690
+ let alive = true;
691
+ AccessibilityInfo.isReduceMotionEnabled()
692
+ .then((r) => alive && setReduce(r))
693
+ .catch(() => {});
694
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
695
+ return () => {
696
+ alive = false;
697
+ sub.remove();
698
+ };
699
+ }, []);
700
+ return reduce;
701
+ }
702
+
674
703
  const styles = StyleSheet.create({
675
704
  root: { alignSelf: 'stretch' },
676
705
  row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
@@ -699,7 +728,7 @@ const styles = StyleSheet.create({
699
728
  },
700
729
  bubbleWrap: { position: 'absolute', width: BUBBLE_W, alignItems: 'center', transformOrigin: 'bottom' },
701
730
  bubble: {
702
- height: BUBBLE_H,
731
+ minHeight: BUBBLE_H,
703
732
  paddingHorizontal: 10,
704
733
  minWidth: 36,
705
734
  alignItems: 'center',
@@ -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
  };
@@ -694,25 +694,30 @@ function Ellipsis({
694
694
  color: string;
695
695
  fontSize: number;
696
696
  }) {
697
+ const { font } = useKit();
698
+ const ff = font.family ? { fontFamily: font.family } : null;
697
699
  const t = useLoop(1400 / speed, linear, true, !reduce);
698
700
  const base = text.replace(/(…|\.{1,3})\s*$/, '');
699
701
  return (
700
702
  <View style={styles.ellipsis}>
701
- <Text style={[styles.label, { color, fontSize }]}>{base}</Text>
703
+ <Text style={[styles.label, ff, { color, fontSize }]}>{base}</Text>
702
704
  {[0, 1, 2].map((i) => (
703
705
  <Animated.Text
704
706
  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
- }}
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
+ ]}
716
721
  >
717
722
  .
718
723
  </Animated.Text>
@@ -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
  };
@@ -6,6 +6,7 @@ import {
6
6
  Pressable,
7
7
  StyleSheet,
8
8
  View,
9
+ useWindowDimensions,
9
10
  type LayoutChangeEvent,
10
11
  type StyleProp,
11
12
  type TextStyle,
@@ -67,6 +68,9 @@ const SIZES: Record<StatefulButtonSize, { h: number; px: number; font: number; i
67
68
  lg: { h: 54, px: 22, font: 17, icon: 20, gap: 9 },
68
69
  };
69
70
  const STAGGER = 22;
71
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
72
+ // content text scales all the way.
73
+ const FONT_CAP = 1.35;
70
74
  const END = 1e6;
71
75
  // Connected scripts (Arabic, Indic, Thai…) roll as one piece instead of letter by letter.
72
76
  const CONNECTED = /[֐-ࣿऀ-෿฀-໿ༀ-࿿က-႟ក-៿יִ-﷿ﹰ-]/;
@@ -131,6 +135,7 @@ function Letters({ text, style, lh, space, anims, onLayout }: LettersProps) {
131
135
  <View key={i} style={{ height: lh, overflow: 'hidden' }}>
132
136
  <Animated.Text
133
137
  numberOfLines={1}
138
+ maxFontSizeMultiplier={FONT_CAP}
134
139
  style={[style, anims?.[i] && { opacity: anims[i].opacity, transform: [{ translateY: anims[i].translateY }] }]}
135
140
  >
136
141
  {ch}
@@ -252,8 +257,11 @@ export function StatefulButton({
252
257
  const reduce = useReduceMotion();
253
258
  const s = SIZES[size];
254
259
  const fontSize = StyleSheet.flatten(textStyle)?.fontSize ?? s.font;
255
- const lh = Math.round(fontSize * 1.3);
256
- const space = fontSize * 0.28;
260
+ // Letter boxes clip to one line, so they grow with the (capped) text size; the Text's own lineHeight scales itself.
261
+ const textScale = Math.min(Math.max(useWindowDimensions().fontScale || 1, 1), FONT_CAP);
262
+ const lineHeight = Math.round(fontSize * 1.3);
263
+ const lh = Math.ceil(lineHeight * textScale);
264
+ const space = fontSize * 0.28 * textScale;
257
265
 
258
266
  const [inner, setInner] = useState<StatefulButtonState>('idle');
259
267
  const current = state ?? inner;
@@ -421,7 +429,7 @@ export function StatefulButton({
421
429
  setWidths((prev) => (prev[text] === w ? prev : { ...prev, [text]: w }));
422
430
  };
423
431
 
424
- const textBase: StyleProp<TextStyle> = [styles.label, { fontSize: s.font, fontFamily: font.family }, textStyle, { lineHeight: lh }];
432
+ const textBase: StyleProp<TextStyle> = [styles.label, { fontSize: s.font, fontFamily: font.family }, textStyle, { lineHeight }];
425
433
  // Press feedback: a light wash on dark fills, a faint shade on light or transparent ones.
426
434
  const wash = variant === 'primary' && readableOn(look[0].bg) === '#ffffff' ? alpha('#ffffff', 0.14) : alpha(colors.foreground, 0.07);
427
435
  const corner = pill ? radius.capsule(s.h) : radius.md;
@@ -444,7 +452,7 @@ export function StatefulButton({
444
452
  style={[
445
453
  styles.surface,
446
454
  {
447
- height: s.h,
455
+ minHeight: s.h,
448
456
  minWidth: s.h,
449
457
  paddingHorizontal: s.px,
450
458
  borderRadius: corner,
@@ -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
  };