@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,5 +1,6 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  Animated,
4
5
  Dimensions,
5
6
  Easing,
@@ -135,6 +136,9 @@ const EDGE = 12; // keep the dropdown this far from the window edges
135
136
  const PANEL_PAD = 6;
136
137
  const SEARCH_H = 46;
137
138
  const FOOT_H = 48;
139
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
140
+ // content text scales all the way.
141
+ const FONT_CAP = 1.35;
138
142
 
139
143
  const normalize = <T extends string | number>(o: T | SelectOption<T>): SelectOption<T> =>
140
144
  typeof o === 'object' ? o : { label: String(o), value: o };
@@ -190,6 +194,7 @@ export function Select<T extends string | number = string>({
190
194
  optionTextStyle,
191
195
  }: SelectProps<T>) {
192
196
  const { colors, radius, dark, font } = useKit();
197
+ const reduce = useReduceMotion();
193
198
  const ff = font.family ? { fontFamily: font.family } : null;
194
199
  const tint = accent ?? colors.accent;
195
200
  const bg = surface ?? colors.background;
@@ -219,13 +224,16 @@ export function Select<T extends string | number = string>({
219
224
  const [anchor, setAnchor] = useState({ x: 0, y: 0, w: 0, h: 0 });
220
225
  const [hi, setHi] = useState(-1); // highlighted row (keyboard / hover, web)
221
226
  const [kb, setKb] = useState(0); // on-screen keyboard height while the dropdown is open
222
- const fieldRef = useRef<View>(null);
223
- const scrollRef = useRef<ScrollView>(null);
227
+ const fieldRef = useRef<ComponentRef<typeof View>>(null);
228
+ const scrollRef = useRef<ComponentRef<typeof ScrollView>>(null);
224
229
  const rowBox = useRef<Record<string, { y: number; h: number }>>({});
225
230
  const scrollY = useRef(0);
226
231
  const viewH = useRef(0);
227
232
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
228
233
  const alive = useRef(true);
234
+ // Read by the sheet's pan responder, which is created once.
235
+ const reduceRef = useRef(reduce);
236
+ reduceRef.current = reduce;
229
237
 
230
238
  const floating = open || picked.length > 0;
231
239
  const float = useRef(new Animated.Value(floating ? 1 : 0)).current; // native
@@ -244,29 +252,35 @@ export function Select<T extends string | number = string>({
244
252
  }, []);
245
253
 
246
254
  useEffect(() => {
247
- Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
248
- }, [floating, float]);
255
+ if (reduce) float.setValue(floating ? 1 : 0);
256
+ else Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
257
+ }, [floating, float, reduce]);
249
258
 
250
259
  useEffect(() => {
251
260
  Animated.timing(focus, { toValue: open ? 1 : 0, duration: 160, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
252
- Animated.spring(halo, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
253
- }, [open, focus, halo]);
261
+ // Reduce Motion: the halo only fades (its scale is pinned below).
262
+ if (reduce) Animated.timing(halo, { toValue: open ? 1 : 0, duration: 150, useNativeDriver: true }).start();
263
+ else Animated.spring(halo, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
264
+ }, [open, focus, halo, reduce]);
254
265
 
255
266
  useEffect(() => {
256
- if (!error) return;
267
+ if (!error || reduce) return;
257
268
  shake.setValue(0);
258
269
  Animated.sequence(
259
270
  [10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))
260
271
  ).start();
261
- }, [error, shake]);
272
+ }, [error, shake, reduce]);
262
273
 
263
274
  useEffect(() => {
264
275
  if (open) {
265
276
  drag.setValue(0);
266
- if (asSheet) Animated.spring(show, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start();
277
+ // Reduce Motion: the panel's slide / scale is pinned below, so `show` only fades it in place.
278
+ if (reduce) Animated.timing(show, { toValue: 1, duration: 150, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
279
+ else if (asSheet) Animated.spring(show, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start();
267
280
  else Animated.timing(show, { toValue: 1, duration: 170, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
268
281
  } else if (mounted) {
269
- Animated.timing(show, { toValue: 0, duration: asSheet ? motion.fast + 40 : 120, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(
282
+ const duration = asSheet ? (reduce ? 150 : motion.fast + 40) : 120;
283
+ Animated.timing(show, { toValue: 0, duration, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(
270
284
  ({ finished }) => finished && setMounted(false)
271
285
  );
272
286
  }
@@ -451,19 +465,21 @@ export function Select<T extends string | number = string>({
451
465
  }, [open]);
452
466
 
453
467
  // Drag the sheet's grabber down to dismiss.
454
- const pan = useMemo(
455
- () =>
456
- PanResponder.create({
457
- onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
458
- onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
459
- onPanResponderRelease: (_, g) => {
460
- if (g.dy > 110 || g.vy > 1.1) setOpen(false);
461
- else Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
462
- },
463
- onPanResponderTerminate: () => Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start(),
464
- }),
465
- [drag]
466
- );
468
+ const pan = useMemo(() => {
469
+ const back = () =>
470
+ reduceRef.current
471
+ ? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
472
+ : Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
473
+ return PanResponder.create({
474
+ onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
475
+ onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
476
+ onPanResponderRelease: (_, g) => {
477
+ if (g.dy > 110 || g.vy > 1.1) setOpen(false);
478
+ else back();
479
+ },
480
+ onPanResponderTerminate: back,
481
+ });
482
+ }, [drag]);
467
483
 
468
484
  const active = error ? colors.danger : tint;
469
485
  const borderColor = focus.interpolate({
@@ -499,6 +515,7 @@ export function Select<T extends string | number = string>({
499
515
  autoFocus={isWeb}
500
516
  returnKeyType="done"
501
517
  accessibilityLabel={searchPlaceholder}
518
+ maxFontSizeMultiplier={FONT_CAP}
502
519
  style={[styles.searchInput, ff, { color: colors.foreground, fontSize: dense ? 15 : 16 }, webInput]}
503
520
  />
504
521
  {query ? (
@@ -553,6 +570,7 @@ export function Select<T extends string | number = string>({
553
570
  rowRadius={rowRadius}
554
571
  query={q}
555
572
  tint={tint}
573
+ reduce={reduce}
556
574
  rowStyle={optionStyle}
557
575
  textStyle={optionTextStyle}
558
576
  onPress={() => choose(o)}
@@ -591,17 +609,21 @@ export function Select<T extends string | number = string>({
591
609
  { borderRadius: radius.capsule(32), opacity: many.length ? 1 : 0.4, backgroundColor: pressed ? colors.muted : 'transparent' },
592
610
  ]}
593
611
  >
594
- <Text style={[styles.footDenseText, ff, { color: colors.mutedForeground }]}>Clear</Text>
612
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footDenseText, ff, { color: colors.mutedForeground }]}>
613
+ Clear
614
+ </Text>
595
615
  </Pressable>
596
616
  <View style={styles.footMid}>
597
- <CountBadge count={many.length} tint={tint} />
617
+ <CountBadge count={many.length} tint={tint} reduce={reduce} />
598
618
  </View>
599
619
  <Pressable
600
620
  onPress={close}
601
621
  accessibilityRole="button"
602
622
  style={({ pressed }) => [styles.footSolid, { backgroundColor: tint, borderRadius: radius.capsule(32), opacity: pressed ? 0.85 : 1 }]}
603
623
  >
604
- <Text style={[styles.footDenseText, styles.footStrong, ff, { color: readableOn(tint) }]}>Done</Text>
624
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footDenseText, styles.footStrong, ff, { color: readableOn(tint) }]}>
625
+ Done
626
+ </Text>
605
627
  </Pressable>
606
628
  </View>
607
629
  ) : (
@@ -612,14 +634,16 @@ export function Select<T extends string | number = string>({
612
634
  accessibilityRole="button"
613
635
  style={({ pressed }) => [styles.footBtn, { borderRadius: radius.md, opacity: many.length ? (pressed ? 0.6 : 1) : 0.4 }]}
614
636
  >
615
- <Text style={[styles.footText, ff, { color: colors.foreground }]}>Clear</Text>
637
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>
638
+ Clear
639
+ </Text>
616
640
  </Pressable>
617
641
  <Pressable
618
642
  onPress={close}
619
643
  accessibilityRole="button"
620
644
  style={({ pressed }) => [styles.footBtn, styles.footMain, { backgroundColor: tint, borderRadius: radius.md, opacity: pressed ? 0.85 : 1 }]}
621
645
  >
622
- <Text style={[styles.footText, styles.footStrong, styles.tabular, ff, { color: readableOn(tint) }]}>
646
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, styles.footStrong, styles.tabular, ff, { color: readableOn(tint) }]}>
623
647
  {many.length ? `Done · ${many.length}` : 'Done'}
624
648
  </Text>
625
649
  </Pressable>
@@ -691,14 +715,14 @@ export function Select<T extends string | number = string>({
691
715
  borderRadius: fieldRadius + 4,
692
716
  borderColor: alpha(active, 0.2),
693
717
  opacity: halo,
694
- transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
718
+ transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.97, 1] }) }],
695
719
  },
696
720
  ]}
697
721
  />
698
722
  <Animated.View
699
723
  style={[
700
724
  styles.field,
701
- { height: HEIGHT, borderRadius: fieldRadius, backgroundColor: bg },
725
+ { minHeight: HEIGHT, borderRadius: fieldRadius, backgroundColor: bg },
702
726
  userField,
703
727
  { borderColor: hovered && !open && !error ? colors.mutedForeground : borderColor },
704
728
  ]}
@@ -707,13 +731,13 @@ export function Select<T extends string | number = string>({
707
731
  {showChips ? (
708
732
  <View style={[styles.chips, { paddingLeft: sz.pad - 6, paddingRight: sz.pad + sz.icon + 8 }]}>
709
733
  {chips.map(({ key, item, leaving, appear }) => (
710
- <AnimatedChip key={key} leaving={leaving} appear={appear} gap={6} shrink={!!item.option} onGone={() => chipGone(key)}>
734
+ <AnimatedChip key={key} leaving={leaving} appear={appear} gap={6} shrink={!!item.option} reduce={reduce} onGone={() => chipGone(key)}>
711
735
  {item.option ? (
712
736
  <View
713
737
  style={[
714
738
  styles.chip,
715
739
  {
716
- height: sz.chip,
740
+ minHeight: sz.chip,
717
741
  paddingLeft: item.option.icon ? 3 : 10,
718
742
  paddingRight: removableChips && !disabled ? 4 : 10,
719
743
  backgroundColor: chipBg,
@@ -726,7 +750,7 @@ export function Select<T extends string | number = string>({
726
750
  {item.option.icon}
727
751
  </View>
728
752
  ) : null}
729
- <Text numberOfLines={1} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont }]}>
753
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont }]}>
730
754
  {item.option.label}
731
755
  </Text>
732
756
  {removableChips && !disabled ? (
@@ -753,12 +777,18 @@ export function Select<T extends string | number = string>({
753
777
  <View
754
778
  accessible
755
779
  accessibilityLabel={maxChips > 0 ? `${item.more} more` : `${item.more} selected`}
756
- style={[styles.chip, styles.more, { height: sz.chip, backgroundColor: chipBg, borderRadius: radius.capsule(sz.chip) }]}
780
+ style={[styles.chip, styles.more, { minHeight: sz.chip, backgroundColor: chipBg, borderRadius: radius.capsule(sz.chip) }]}
757
781
  >
758
- {maxChips > 0 ? <Text style={[styles.chipText, styles.tabular, ff, { color: tint, fontSize: sz.chipFont }]}>+</Text> : null}
759
- <RollingNumber value={item.more} distance={sz.chipFont} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont }]} />
782
+ {maxChips > 0 ? (
783
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.chipText, styles.tabular, ff, { color: tint, fontSize: sz.chipFont }]}>
784
+ +
785
+ </Text>
786
+ ) : null}
787
+ <RollingNumber value={item.more} distance={sz.chipFont} reduce={reduce} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont }]} />
760
788
  {maxChips > 0 ? null : (
761
- <Text style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont, marginLeft: 3 }]}>selected</Text>
789
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont, marginLeft: 3 }]}>
790
+ selected
791
+ </Text>
762
792
  )}
763
793
  </View>
764
794
  )}
@@ -768,6 +798,7 @@ export function Select<T extends string | number = string>({
768
798
  ) : (
769
799
  <Text
770
800
  numberOfLines={1}
801
+ maxFontSizeMultiplier={FONT_CAP}
771
802
  style={[
772
803
  ff,
773
804
  {
@@ -802,17 +833,21 @@ export function Select<T extends string | number = string>({
802
833
  ]}
803
834
  >
804
835
  {/* The notch: the label paints the surface over the border, so it matches the rendered width. */}
805
- <Animated.Text numberOfLines={1} style={[styles.label, ff, { fontSize: sz.font, backgroundColor: bg }, labelStyle, { color: labelColor }]}>
836
+ <Animated.Text
837
+ numberOfLines={1}
838
+ maxFontSizeMultiplier={FONT_CAP}
839
+ style={[styles.label, ff, { fontSize: sz.font, backgroundColor: bg }, labelStyle, { color: labelColor }]}
840
+ >
806
841
  {label}
807
842
  </Animated.Text>
808
843
  </Animated.View>
809
844
  ) : null}
810
- <Chevron open={open} size={sz.icon} right={sz.pad - 4} color={open ? active : (iconColor ?? colors.mutedForeground)} />
845
+ <Chevron open={open} reduce={reduce} size={sz.icon} right={sz.pad - 4} color={open ? active : (iconColor ?? colors.mutedForeground)} />
811
846
  </Animated.View>
812
847
  </>
813
848
  )}
814
849
  </Pressable>
815
- {message ? <FieldMessage text={message} error={!!error} style={messageStyle} /> : null}
850
+ {message ? <FieldMessage text={message} error={!!error} reduce={reduce} style={messageStyle} /> : null}
816
851
 
817
852
  <Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
818
853
  <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: asSheet ? colors.overlay : 'transparent', opacity: show }]}>
@@ -838,11 +873,15 @@ export function Select<T extends string | number = string>({
838
873
  ? {
839
874
  opacity: show,
840
875
  transform: [
841
- { scale: show.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] }) },
842
- { translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [16, 0] }), drag) },
876
+ { scale: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.94, 1] }) },
877
+ { translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 16, 0] }), drag) },
843
878
  ],
844
879
  }
845
- : { transform: [{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [winH * 0.8, 0] }), drag) }] },
880
+ : {
881
+ // Reduce Motion: fade in place instead of sliding up.
882
+ opacity: reduce ? show : 1,
883
+ transform: [{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : winH * 0.8, 0] }), drag) }],
884
+ },
846
885
  ]}
847
886
  >
848
887
  <View {...pan.panHandlers} style={styles.sheetHeader}>
@@ -851,7 +890,7 @@ export function Select<T extends string | number = string>({
851
890
  {multiple ? (
852
891
  <View style={styles.subtitleRow}>
853
892
  {many.length ? (
854
- <CountBadge count={many.length} tint={tint} />
893
+ <CountBadge count={many.length} tint={tint} reduce={reduce} />
855
894
  ) : (
856
895
  <Text style={[styles.subtitle, ff, { color: colors.mutedForeground }]}>Choose one or more</Text>
857
896
  )}
@@ -880,8 +919,8 @@ export function Select<T extends string | number = string>({
880
919
  {
881
920
  opacity: show,
882
921
  transform: [
883
- { translateY: show.interpolate({ inputRange: [0, 1], outputRange: [below ? -6 : 6, 0] }) },
884
- { scale: show.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) },
922
+ { translateY: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : below ? -6 : 6, 0] }) },
923
+ { scale: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.97, 1] }) },
885
924
  ],
886
925
  },
887
926
  below ? { top: anchor.y + anchor.h + GAP } : { bottom: Math.max(EDGE, winH - anchor.y + GAP) },
@@ -912,6 +951,7 @@ function OptionRow<T extends string | number>({
912
951
  rowRadius,
913
952
  query,
914
953
  tint,
954
+ reduce,
915
955
  rowStyle,
916
956
  textStyle,
917
957
  onPress,
@@ -926,6 +966,7 @@ function OptionRow<T extends string | number>({
926
966
  rowRadius: number;
927
967
  query: string;
928
968
  tint: string;
969
+ reduce: boolean;
929
970
  rowStyle?: StyleProp<ViewStyle>;
930
971
  textStyle?: StyleProp<TextStyle>;
931
972
  onPress: () => void;
@@ -936,8 +977,10 @@ function OptionRow<T extends string | number>({
936
977
  const ff = font.family ? { fontFamily: font.family } : null;
937
978
  const check = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: scale + opacity
938
979
  useEffect(() => {
939
- Animated.spring(check, { toValue: selected ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
940
- }, [selected, check]);
980
+ // Reduce Motion: the check only fades (no pop / twist).
981
+ if (reduce) Animated.timing(check, { toValue: selected ? 1 : 0, duration: 150, useNativeDriver: true }).start();
982
+ else Animated.spring(check, { toValue: selected ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
983
+ }, [selected, check, reduce]);
941
984
  const on = readableOn(tint);
942
985
  const box = dense ? 18 : 22;
943
986
  const hoverBg = alpha(colors.foreground, 0.06);
@@ -966,7 +1009,7 @@ function OptionRow<T extends string | number>({
966
1009
  >
967
1010
  {multiple ? (
968
1011
  <View style={[styles.box, { width: box, height: box, borderColor: selected ? tint : colors.input, borderRadius: radius.scale(5) }]}>
969
- <Animated.View style={[styles.boxFill, { backgroundColor: tint, borderRadius: radius.scale(5), opacity: check, transform: [{ scale: check }] }]}>
1012
+ <Animated.View style={[styles.boxFill, { backgroundColor: tint, borderRadius: radius.scale(5), opacity: check, transform: [{ scale: reduce ? 1 : check }] }]}>
970
1013
  <Icon name="check" size={dense ? 12 : 14} color={on} strokeWidth={2.8} />
971
1014
  </Animated.View>
972
1015
  </View>
@@ -994,7 +1037,9 @@ function OptionRow<T extends string | number>({
994
1037
  styles.checkWrap,
995
1038
  {
996
1039
  opacity: check,
997
- transform: [{ scale: check }, { rotate: check.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) }],
1040
+ transform: reduce
1041
+ ? []
1042
+ : [{ scale: check }, { rotate: check.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) }],
998
1043
  },
999
1044
  ]}
1000
1045
  >
@@ -1055,6 +1100,7 @@ function AnimatedChip({
1055
1100
  appear,
1056
1101
  gap,
1057
1102
  shrink,
1103
+ reduce,
1058
1104
  onGone,
1059
1105
  children,
1060
1106
  }: {
@@ -1063,6 +1109,8 @@ function AnimatedChip({
1063
1109
  gap: number;
1064
1110
  /** Whether the slot may shrink when the row is tight (chips yes, the "+N" counter no). */
1065
1111
  shrink: boolean;
1112
+ /** Reduce Motion: fade only — no pop, and neighbours close the gap at once. */
1113
+ reduce: boolean;
1066
1114
  onGone: () => void;
1067
1115
  children: React.ReactNode;
1068
1116
  }) {
@@ -1076,25 +1124,27 @@ function AnimatedChip({
1076
1124
  let alive = true;
1077
1125
  let anims: Animated.CompositeAnimation[];
1078
1126
  if (leaving) {
1079
- let left = 2;
1127
+ anims = [Animated.timing(pop, { toValue: 0, duration: 150, easing: Easing.out(Easing.quad), useNativeDriver: true })];
1128
+ if (!reduce) anims.push(Animated.timing(size, { toValue: 0, duration: 230, easing: Easing.inOut(Easing.cubic), useNativeDriver: false }));
1129
+ let left = anims.length;
1080
1130
  const end = ({ finished }: { finished: boolean }) => {
1081
1131
  if (finished && alive && --left === 0) gone.current();
1082
1132
  };
1083
- anims = [
1084
- Animated.timing(pop, { toValue: 0, duration: 150, easing: Easing.out(Easing.quad), useNativeDriver: true }),
1085
- Animated.timing(size, { toValue: 0, duration: 230, easing: Easing.inOut(Easing.cubic), useNativeDriver: false }),
1086
- ];
1087
1133
  anims.forEach((a) => a.start(end));
1088
1134
  } else {
1089
1135
  size.setValue(1);
1090
- anims = [Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.snappy })];
1136
+ anims = [
1137
+ reduce
1138
+ ? Animated.timing(pop, { toValue: 1, duration: 150, useNativeDriver: true })
1139
+ : Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.snappy }),
1140
+ ];
1091
1141
  anims.forEach((a) => a.start());
1092
1142
  }
1093
1143
  return () => {
1094
1144
  alive = false;
1095
1145
  anims.forEach((a) => a.stop());
1096
1146
  };
1097
- }, [leaving, pop, size]);
1147
+ }, [leaving, pop, size, reduce]);
1098
1148
 
1099
1149
  const collapsing = leaving && w > 0;
1100
1150
  return (
@@ -1119,7 +1169,7 @@ function AnimatedChip({
1119
1169
  collapsing && { width: w, flexShrink: 0 },
1120
1170
  {
1121
1171
  opacity: pop.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
1122
- transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [0.85, 1] }) }],
1172
+ transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.85, 1] }) }],
1123
1173
  },
1124
1174
  ]}
1125
1175
  >
@@ -1129,24 +1179,26 @@ function AnimatedChip({
1129
1179
  );
1130
1180
  }
1131
1181
 
1132
- /** A number that rolls (native translateY + opacity) up when it grows and down when it shrinks. */
1133
- function RollingNumber({ value, style, distance = 10 }: { value: number; style?: StyleProp<TextStyle>; distance?: number }) {
1182
+ /** A number that rolls (native translateY + opacity) up when it grows and down when it shrinks. Reduce Motion shows the new value at once. */
1183
+ function RollingNumber({ value, style, distance = 10, reduce }: { value: number; style?: StyleProp<TextStyle>; distance?: number; reduce: boolean }) {
1134
1184
  const [state, setState] = useState({ cur: value, prev: value, dir: 1 });
1135
1185
  if (state.cur !== value) setState({ cur: value, prev: state.cur, dir: value > state.cur ? 1 : -1 });
1136
1186
  // A fresh value per change starts at 0 in the same render, so there's no one-frame flash.
1137
- const roll = useMemo(() => new Animated.Value(state.cur === state.prev ? 1 : 0), [state]);
1187
+ const roll = useMemo(() => new Animated.Value(state.cur !== state.prev && !reduce ? 0 : 1), [state, reduce]);
1188
+ const moving = state.cur !== state.prev && !reduce;
1138
1189
  useEffect(() => {
1190
+ if (!moving) return;
1139
1191
  const a = Animated.spring(roll, { toValue: 1, useNativeDriver: true, ...motion.snappy });
1140
1192
  a.start();
1141
1193
  return () => a.stop();
1142
- }, [roll]);
1143
- const moving = state.cur !== state.prev;
1194
+ }, [roll, moving]);
1144
1195
  return (
1145
1196
  <View style={styles.roll}>
1146
1197
  {moving ? (
1147
1198
  <Animated.Text
1148
1199
  accessible={false}
1149
1200
  importantForAccessibility="no"
1201
+ maxFontSizeMultiplier={FONT_CAP}
1150
1202
  style={[
1151
1203
  style,
1152
1204
  styles.tabular,
@@ -1161,6 +1213,7 @@ function RollingNumber({ value, style, distance = 10 }: { value: number; style?:
1161
1213
  </Animated.Text>
1162
1214
  ) : null}
1163
1215
  <Animated.Text
1216
+ maxFontSizeMultiplier={FONT_CAP}
1164
1217
  style={[
1165
1218
  style,
1166
1219
  styles.tabular,
@@ -1177,7 +1230,7 @@ function RollingNumber({ value, style, distance = 10 }: { value: number; style?:
1177
1230
  }
1178
1231
 
1179
1232
  /** "N selected" pill whose number rolls; it pops out when the count drops to 0. */
1180
- function CountBadge({ count, tint }: { count: number; tint: string }) {
1233
+ function CountBadge({ count, tint, reduce }: { count: number; tint: string; reduce: boolean }) {
1181
1234
  const { radius, font } = useKit();
1182
1235
  const ff = font.family ? { fontFamily: font.family } : null;
1183
1236
  const on = count > 0;
@@ -1185,10 +1238,12 @@ function CountBadge({ count, tint }: { count: number; tint: string }) {
1185
1238
  const last = useRef(count);
1186
1239
  if (on) last.current = count;
1187
1240
  useEffect(() => {
1188
- const a = Animated.spring(vis, { toValue: on ? 1 : 0, useNativeDriver: true, ...motion.snappy });
1241
+ const a = reduce
1242
+ ? Animated.timing(vis, { toValue: on ? 1 : 0, duration: 150, useNativeDriver: true })
1243
+ : Animated.spring(vis, { toValue: on ? 1 : 0, useNativeDriver: true, ...motion.snappy });
1189
1244
  a.start();
1190
1245
  return () => a.stop();
1191
- }, [on, vis]);
1246
+ }, [on, vis, reduce]);
1192
1247
  const text = [styles.badgeText, ff, { color: tint }];
1193
1248
  return (
1194
1249
  <Animated.View
@@ -1201,12 +1256,14 @@ function CountBadge({ count, tint }: { count: number; tint: string }) {
1201
1256
  backgroundColor: alpha(tint, 0.12),
1202
1257
  borderRadius: radius.capsule(24),
1203
1258
  opacity: vis.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
1204
- transform: [{ scale: vis.interpolate({ inputRange: [0, 1], outputRange: [0.85, 1] }) }],
1259
+ transform: [{ scale: vis.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.85, 1] }) }],
1205
1260
  },
1206
1261
  ]}
1207
1262
  >
1208
- <RollingNumber value={last.current} distance={9} style={text} />
1209
- <Text style={[text, styles.badgeSuffix]}>selected</Text>
1263
+ <RollingNumber value={last.current} distance={9} reduce={reduce} style={text} />
1264
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[text, styles.badgeSuffix]}>
1265
+ selected
1266
+ </Text>
1210
1267
  </Animated.View>
1211
1268
  );
1212
1269
  }
@@ -1231,20 +1288,21 @@ function Highlight({ text, query, tint, style }: { text: string; query: string;
1231
1288
  }
1232
1289
 
1233
1290
  /** Helper / error line that fades and slides in; errors get an icon and are announced. */
1234
- function FieldMessage({ text, error, style }: { text: string; error: boolean; style?: StyleProp<TextStyle> }) {
1291
+ function FieldMessage({ text, error, reduce, style }: { text: string; error: boolean; reduce: boolean; style?: StyleProp<TextStyle> }) {
1235
1292
  const { colors, font } = useKit();
1236
1293
  const enter = useRef(new Animated.Value(0)).current; // native
1237
1294
  useEffect(() => {
1238
1295
  enter.setValue(0);
1239
- Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
1240
- }, [text, error, enter]);
1296
+ if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
1297
+ else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
1298
+ }, [text, error, enter, reduce]);
1241
1299
  const color = error ? colors.danger : colors.mutedForeground;
1242
1300
  return (
1243
1301
  <Animated.View
1244
1302
  accessibilityLiveRegion="polite"
1245
1303
  style={[
1246
1304
  styles.message,
1247
- { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }] },
1305
+ { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 4, 0] }) }] },
1248
1306
  ]}
1249
1307
  >
1250
1308
  {error ? <Icon name="alert" size={14} color={color} /> : null}
@@ -1253,11 +1311,12 @@ function FieldMessage({ text, error, style }: { text: string; error: boolean; st
1253
1311
  );
1254
1312
  }
1255
1313
 
1256
- function Chevron({ open, color, size, right }: { open: boolean; color: string; size: number; right: number }) {
1314
+ function Chevron({ open, reduce, color, size, right }: { open: boolean; reduce: boolean; color: string; size: number; right: number }) {
1257
1315
  const turn = useRef(new Animated.Value(open ? 1 : 0)).current;
1258
1316
  useEffect(() => {
1259
- Animated.spring(turn, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
1260
- }, [open, turn]);
1317
+ if (reduce) turn.setValue(open ? 1 : 0);
1318
+ else Animated.spring(turn, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
1319
+ }, [open, turn, reduce]);
1261
1320
  return (
1262
1321
  <Animated.View
1263
1322
  pointerEvents="none"
@@ -1294,6 +1353,22 @@ function Icon({ name, size, color, strokeWidth = 1.8 }: { name: 'chevron' | 'che
1294
1353
  );
1295
1354
  }
1296
1355
 
1356
+ function useReduceMotion() {
1357
+ const [reduce, setReduce] = useState(false);
1358
+ useEffect(() => {
1359
+ let alive = true;
1360
+ AccessibilityInfo.isReduceMotionEnabled()
1361
+ .then((r) => alive && setReduce(r))
1362
+ .catch(() => {});
1363
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
1364
+ return () => {
1365
+ alive = false;
1366
+ sub.remove();
1367
+ };
1368
+ }, []);
1369
+ return reduce;
1370
+ }
1371
+
1297
1372
  const styles = StyleSheet.create({
1298
1373
  field: { borderWidth: 1.5, justifyContent: 'center' },
1299
1374
  halo: { position: 'absolute', top: -5, left: -5, right: -5, bottom: -5, borderWidth: 3.5 },
@@ -1308,7 +1383,7 @@ const styles = StyleSheet.create({
1308
1383
  tabular: { fontVariant: ['tabular-nums'], fontWeight: '400' },
1309
1384
  roll: { overflow: 'hidden' },
1310
1385
  rollPrev: { position: 'absolute', left: 0, top: 0 },
1311
- badge: { flexDirection: 'row', alignItems: 'center', height: 24, paddingHorizontal: 9 },
1386
+ badge: { flexDirection: 'row', alignItems: 'center', minHeight: 24, paddingHorizontal: 9 },
1312
1387
  badgeText: { fontSize: 12.5, fontWeight: '400' },
1313
1388
  badgeSuffix: { marginLeft: 3 },
1314
1389
  chipX: { alignItems: 'center', justifyContent: 'center' },
@@ -1332,11 +1407,11 @@ const styles = StyleSheet.create({
1332
1407
  sheetHeader: { alignItems: 'center', paddingTop: 10, paddingBottom: 12 },
1333
1408
  grabber: { width: 40, height: 5, borderRadius: 3, marginBottom: 12 },
1334
1409
  title: { fontSize: 17, fontWeight: '400', letterSpacing: -0.2, marginTop: 4 },
1335
- subtitleRow: { height: 26, marginTop: 4, alignItems: 'center', justifyContent: 'center' },
1410
+ subtitleRow: { minHeight: 26, marginTop: 4, alignItems: 'center', justifyContent: 'center' },
1336
1411
  subtitle: { fontSize: 13 },
1337
- search: { height: 44, marginBottom: 8, marginHorizontal: 4, flexDirection: 'row', alignItems: 'center', paddingLeft: 12, paddingRight: 10, gap: 8 },
1412
+ search: { minHeight: 44, marginBottom: 8, marginHorizontal: 4, flexDirection: 'row', alignItems: 'center', paddingLeft: 12, paddingRight: 10, gap: 8 },
1338
1413
  searchDense: {
1339
- height: SEARCH_H,
1414
+ minHeight: SEARCH_H,
1340
1415
  flexDirection: 'row',
1341
1416
  alignItems: 'center',
1342
1417
  paddingLeft: 13,
@@ -1344,7 +1419,7 @@ const styles = StyleSheet.create({
1344
1419
  gap: 8,
1345
1420
  borderBottomWidth: StyleSheet.hairlineWidth,
1346
1421
  },
1347
- searchInput: { flex: 1, height: 44, paddingVertical: 0 },
1422
+ searchInput: { flex: 1, minHeight: 44, paddingVertical: 0 },
1348
1423
  clear: { width: 20, height: 20, borderRadius: 10, alignItems: 'center', justifyContent: 'center' },
1349
1424
  scroll: { flexGrow: 0, flexShrink: 1 },
1350
1425
  listContent: { paddingBottom: 6 },
@@ -1359,20 +1434,20 @@ const styles = StyleSheet.create({
1359
1434
  emptyTitle: { fontWeight: '400', textAlign: 'center' },
1360
1435
  emptyText: { fontSize: 13, marginTop: 3, textAlign: 'center' },
1361
1436
  footer: { flexDirection: 'row', gap: 8, paddingTop: 10, paddingHorizontal: 4, borderTopWidth: StyleSheet.hairlineWidth },
1362
- footBtn: { height: 44, paddingHorizontal: 18, alignItems: 'center', justifyContent: 'center' },
1437
+ footBtn: { minHeight: 44, paddingHorizontal: 18, alignItems: 'center', justifyContent: 'center' },
1363
1438
  footMain: { flex: 1 },
1364
1439
  footText: { fontSize: 15, fontWeight: '400' },
1365
1440
  footStrong: { fontWeight: '400' },
1366
1441
  footDense: {
1367
- height: FOOT_H,
1442
+ minHeight: FOOT_H,
1368
1443
  flexDirection: 'row',
1369
1444
  alignItems: 'center',
1370
1445
  paddingHorizontal: 8,
1371
1446
  gap: 8,
1372
1447
  borderTopWidth: StyleSheet.hairlineWidth,
1373
1448
  },
1374
- footGhost: { height: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
1375
- footSolid: { height: 32, paddingHorizontal: 16, alignItems: 'center', justifyContent: 'center' },
1449
+ footGhost: { minHeight: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
1450
+ footSolid: { minHeight: 32, paddingHorizontal: 16, alignItems: 'center', justifyContent: 'center' },
1376
1451
  footMid: { flex: 1, alignItems: 'center', justifyContent: 'center' },
1377
1452
  footDenseText: { fontSize: 14, fontWeight: '400' },
1378
1453
  dropdown: {
@@ -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
  };