@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, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
1
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  Animated,
4
5
  Easing,
5
6
  Platform,
@@ -59,6 +60,10 @@ export interface SearchBarProps extends Omit<TextInputProps, 'style'> {
59
60
  style?: StyleProp<ViewStyle>;
60
61
  }
61
62
 
63
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
64
+ // content text scales all the way.
65
+ const FONT_CAP = 1.35;
66
+
62
67
  /** Every dimension comes from one table so the sizes read clearly different. */
63
68
  const SIZES: Record<SearchBarSize, { h: number; font: number; icon: number; slot: number; r: number; clear: number; end: number }> = {
64
69
  sm: { h: 34, font: 14, icon: 14, slot: 30, r: 9, clear: 16, end: 6 },
@@ -75,7 +80,7 @@ const webNoOutline = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWid
75
80
  * panel drops down while focused. The 'minimal' variant rests as a round icon and expands
76
81
  * into a full field when tapped.
77
82
  */
78
- export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBar(
83
+ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarProps>(function SearchBar(
79
84
  {
80
85
  variant = 'default',
81
86
  size = 'md',
@@ -104,14 +109,16 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
104
109
  },
105
110
  ref
106
111
  ) {
107
- const { colors, radius } = useKit();
112
+ const { colors, radius, font } = useKit();
113
+ const reduce = useReduceMotion();
114
+ const ff = font.family ? { fontFamily: font.family } : null;
108
115
  const s = SIZES[size];
109
116
  const tint = accent ?? colors.accent;
110
117
  const minimal = variant === 'minimal';
111
118
  const cancelEnabled = !minimal && (showCancel ?? true);
112
119
 
113
- const inputRef = useRef<TextInput>(null);
114
- useImperativeHandle(ref, () => inputRef.current as TextInput);
120
+ const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
121
+ useImperativeHandle(ref, () => inputRef.current as ComponentRef<typeof TextInput>);
115
122
 
116
123
  const [focused, setFocused] = useState(false);
117
124
  const [inner, setInner] = useState(defaultValue ?? '');
@@ -139,28 +146,47 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
139
146
  useEffect(() => {
140
147
  const to = focused ? 1 : 0;
141
148
  Animated.timing(focusJS, { toValue: to, duration: motion.fast, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
149
+ // Reduce Motion: the halo only fades (its scale is pinned below) and the magnifier doesn't pop.
150
+ if (reduce) {
151
+ Animated.timing(halo, { toValue: to, duration: 150, useNativeDriver: true }).start();
152
+ return;
153
+ }
142
154
  Animated.spring(halo, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
143
155
  if (focused) {
144
156
  icon.setValue(0.6);
145
157
  Animated.spring(icon, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
146
158
  }
147
- }, [focused, focusJS, halo, icon]);
159
+ }, [focused, focusJS, halo, icon, reduce]);
148
160
 
149
161
  useEffect(() => {
150
162
  const to = cancelShown ? 1 : 0;
163
+ // Reduce Motion: the field narrows at once and Cancel fades in place.
164
+ if (reduce) {
165
+ cancelJS.setValue(to);
166
+ Animated.timing(cancelN, { toValue: to, duration: 150, useNativeDriver: true }).start();
167
+ return;
168
+ }
151
169
  Animated.spring(cancelJS, { toValue: to, useNativeDriver: false, ...motion.snappy }).start();
152
170
  Animated.spring(cancelN, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
153
- }, [cancelShown, cancelJS, cancelN]);
171
+ }, [cancelShown, cancelJS, cancelN, reduce]);
154
172
 
155
173
  useEffect(() => {
156
174
  const to = open ? 1 : 0;
175
+ // Reduce Motion: 'minimal' snaps to its new width; the contents still fade.
176
+ if (reduce) {
177
+ openJS.setValue(to);
178
+ Animated.timing(openN, { toValue: to, duration: 150, useNativeDriver: true }).start();
179
+ return;
180
+ }
157
181
  Animated.spring(openJS, { toValue: to, useNativeDriver: false, ...motion.gentle }).start();
158
182
  Animated.timing(openN, { toValue: to, duration: open ? motion.normal : motion.fast, delay: open ? 80 : 0, useNativeDriver: true }).start();
159
- }, [open, openJS, openN]);
183
+ }, [open, openJS, openN, reduce]);
160
184
 
161
185
  useEffect(() => {
162
- Animated.spring(clear, { toValue: text && editable && open ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
163
- }, [text, editable, open, clear]);
186
+ const to = text && editable && open ? 1 : 0;
187
+ if (reduce) Animated.timing(clear, { toValue: to, duration: 150, useNativeDriver: true }).start();
188
+ else Animated.spring(clear, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
189
+ }, [text, editable, open, clear, reduce]);
164
190
 
165
191
  useEffect(() => {
166
192
  Animated.timing(busy, { toValue: loading ? 1 : 0, duration: motion.fast, useNativeDriver: true }).start();
@@ -229,7 +255,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
229
255
  onLayout={(e) => setRowW(e.nativeEvent.layout.width)}
230
256
  style={[styles.row, minimal && { justifyContent: align === 'right' ? 'flex-end' : 'flex-start' }]}
231
257
  >
232
- <Animated.View style={minimal ? { width: fieldW, height: s.h } : [styles.flex, { height: s.h }]}>
258
+ <Animated.View style={minimal ? { width: fieldW, minHeight: s.h } : [styles.flex, { minHeight: s.h }]}>
233
259
  <Animated.View
234
260
  pointerEvents="none"
235
261
  style={[
@@ -238,7 +264,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
238
264
  borderRadius: r + 4,
239
265
  backgroundColor: alpha(tint, 0.18),
240
266
  opacity: halo,
241
- transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) }],
267
+ transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.96, 1] }) }],
242
268
  },
243
269
  ]}
244
270
  />
@@ -275,6 +301,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
275
301
  autoCorrect={false}
276
302
  accessibilityRole="search"
277
303
  accessibilityLabel={props.accessibilityLabel ?? placeholder}
304
+ maxFontSizeMultiplier={FONT_CAP}
278
305
  {...props}
279
306
  autoFocus={autoFocus}
280
307
  editable={editable}
@@ -293,12 +320,12 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
293
320
  if (minimal && !text) setExpanded(false);
294
321
  onBlur?.(e);
295
322
  }}
296
- style={[styles.input, { color: colors.foreground, fontSize: s.font, height: s.h - 3 }, webNoOutline]}
323
+ style={[styles.input, ff, { color: colors.foreground, fontSize: s.font, minHeight: s.h - 3 }, webNoOutline]}
297
324
  />
298
325
  </Animated.View>
299
326
  <Animated.View
300
327
  pointerEvents={text && open ? 'auto' : 'none'}
301
- style={[styles.clear, { width: s.clear + 8, opacity: clear, transform: [{ scale: clear }] }]}
328
+ style={[styles.clear, { width: s.clear + 8, opacity: clear, transform: [{ scale: reduce ? 1 : clear }] }]}
302
329
  >
303
330
  <Pressable
304
331
  hitSlop={12}
@@ -341,9 +368,10 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
341
368
  {cancelEnabled ? (
342
369
  <>
343
370
  <Text
371
+ maxFontSizeMultiplier={FONT_CAP}
344
372
  pointerEvents="none"
345
373
  onLayout={(e) => setCancelW(Math.ceil(e.nativeEvent.layout.width) + 14)}
346
- style={[styles.cancelText, styles.measure, { fontSize: s.font }]}
374
+ style={[styles.cancelText, styles.measure, ff, { fontSize: s.font }]}
347
375
  >
348
376
  {cancelLabel}
349
377
  </Text>
@@ -356,7 +384,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
356
384
  {
357
385
  width: cancelW,
358
386
  opacity: cancelN,
359
- transform: [{ translateX: cancelN.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }],
387
+ transform: [{ translateX: cancelN.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 24, 0] }) }],
360
388
  },
361
389
  ]}
362
390
  >
@@ -366,7 +394,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
366
394
  accessibilityLabel={cancelLabel}
367
395
  onPress={cancel}
368
396
  >
369
- <Text numberOfLines={1} style={[styles.cancelText, { color: tint, fontSize: s.font }]}>
397
+ <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={[styles.cancelText, ff, { color: tint, fontSize: s.font }]}>
370
398
  {cancelLabel}
371
399
  </Text>
372
400
  </Pressable>
@@ -377,27 +405,37 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
377
405
  </View>
378
406
 
379
407
  {suggestions ? (
380
- <SuggestionPanel open={panelOpen && open && visible.length > 0} items={visible} query={query} tint={tint} onSelect={select} />
408
+ <SuggestionPanel
409
+ open={panelOpen && open && visible.length > 0}
410
+ items={visible}
411
+ query={query}
412
+ tint={tint}
413
+ reduce={reduce}
414
+ onSelect={select}
415
+ />
381
416
  ) : null}
382
417
  </View>
383
418
  );
384
419
  });
385
420
 
386
- /** Dropdown list under the bar: drops in, rows show an icon, bolded match and a hint. */
421
+ /** Dropdown list under the bar: drops in (fades with Reduce Motion), rows show an icon, bolded match and a hint. */
387
422
  function SuggestionPanel({
388
423
  open,
389
424
  items,
390
425
  query,
391
426
  tint,
427
+ reduce,
392
428
  onSelect,
393
429
  }: {
394
430
  open: boolean;
395
431
  items: SearchSuggestion[];
396
432
  query: string;
397
433
  tint: string;
434
+ reduce: boolean;
398
435
  onSelect: (item: SearchSuggestion) => void;
399
436
  }) {
400
- const { colors, radius } = useKit();
437
+ const { colors, radius, font } = useKit();
438
+ const ff = font.family ? { fontFamily: font.family } : null;
401
439
  const [mounted, setMounted] = useState(open);
402
440
  const enter = useRef(new Animated.Value(0)).current;
403
441
  const last = useRef(items);
@@ -406,13 +444,14 @@ function SuggestionPanel({
406
444
  useEffect(() => {
407
445
  if (open) {
408
446
  setMounted(true);
409
- Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
447
+ if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
448
+ else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
410
449
  } else {
411
450
  Animated.timing(enter, { toValue: 0, duration: motion.fast, useNativeDriver: true }).start(({ finished }) => {
412
451
  if (finished) setMounted(false);
413
452
  });
414
453
  }
415
- }, [open, enter]);
454
+ }, [open, enter, reduce]);
416
455
 
417
456
  if (!mounted) return null;
418
457
  return (
@@ -426,8 +465,8 @@ function SuggestionPanel({
426
465
  borderRadius: radius.lg,
427
466
  opacity: enter,
428
467
  transform: [
429
- { translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [-6, 0] }) },
430
- { scale: enter.interpolate({ inputRange: [0, 1], outputRange: [0.98, 1] }) },
468
+ { translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : -6, 0] }) },
469
+ { scale: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.98, 1] }) },
431
470
  ],
432
471
  },
433
472
  ]}
@@ -443,11 +482,11 @@ function SuggestionPanel({
443
482
  <View style={[styles.optionIcon, { backgroundColor: colors.muted }]}>
444
483
  <Icon name={item.icon ?? 'search'} size={15} color={item.icon === 'trending' ? tint : colors.mutedForeground} />
445
484
  </View>
446
- <Text numberOfLines={1} style={[styles.optionText, { color: colors.mutedForeground }]}>
485
+ <Text numberOfLines={1} style={[styles.optionText, ff, { color: colors.mutedForeground }]}>
447
486
  <Highlight label={item.label} query={query} color={colors.foreground} />
448
487
  </Text>
449
488
  {item.hint ? (
450
- <Text numberOfLines={1} style={[styles.hint, { color: colors.mutedForeground }]}>
489
+ <Text numberOfLines={1} style={[styles.hint, ff, { color: colors.mutedForeground }]}>
451
490
  {item.hint}
452
491
  </Text>
453
492
  ) : null}
@@ -496,6 +535,22 @@ function Icon({ name, size, color }: { name: 'search' | 'recent' | 'trending'; s
496
535
  );
497
536
  }
498
537
 
538
+ function useReduceMotion() {
539
+ const [reduce, setReduce] = useState(false);
540
+ useEffect(() => {
541
+ let alive = true;
542
+ AccessibilityInfo.isReduceMotionEnabled()
543
+ .then((r) => alive && setReduce(r))
544
+ .catch(() => {});
545
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
546
+ return () => {
547
+ alive = false;
548
+ sub.remove();
549
+ };
550
+ }, []);
551
+ return reduce;
552
+ }
553
+
499
554
  const styles = StyleSheet.create({
500
555
  root: { alignSelf: 'stretch' },
501
556
  disabled: { opacity: 0.5 },
@@ -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,6 +1,7 @@
1
1
  import * as Haptics from 'expo-haptics';
2
2
  import React, { useEffect, useId, useMemo, useRef, useState } from 'react';
3
3
  import {
4
+ AccessibilityInfo,
4
5
  Animated,
5
6
  PanResponder,
6
7
  Pressable,
@@ -48,6 +49,10 @@ export interface SegmentedControlProps {
48
49
  style?: StyleProp<ViewStyle>;
49
50
  }
50
51
 
52
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
53
+ // content text scales all the way.
54
+ const FONT_CAP = 1.35;
55
+
51
56
  const SIZES: Record<SegmentedControlSize, { h: number; font: number; px: number; icon: number }> = {
52
57
  sm: { h: 32, font: 13, px: 12, icon: 14 },
53
58
  md: { h: 40, font: 14.5, px: 16, icon: 16 },
@@ -57,7 +62,7 @@ const SIZES: Record<SegmentedControlSize, { h: number; font: number; px: number;
57
62
  const INSET = 3;
58
63
  // Liquid move: the edge in the direction of travel leads, the other one trails behind.
59
64
  const LEAD = motion.snappy;
60
- const TRAIL = { damping: 20, stiffness: 150, mass: 0.9 };
65
+ const TRAIL = { damping: 24, stiffness: 150, mass: 0.9 };
61
66
 
62
67
  /**
63
68
  * Segmented control with a liquid indicator: its leading edge springs ahead and the trailing
@@ -81,6 +86,7 @@ export function SegmentedControl({
81
86
  }: SegmentedControlProps) {
82
87
  const { colors, radius, dark } = useKit();
83
88
  const gid = 'sc' + useId().replace(/[^a-zA-Z0-9]/g, '');
89
+ const reduce = useReduceMotion();
84
90
  const items: SegmentOption[] = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
85
91
  const [inner, setInner] = useState(defaultValue ?? items[0]?.value);
86
92
  const selected = value ?? inner;
@@ -98,7 +104,8 @@ export function SegmentedControl({
98
104
  useEffect(() => {
99
105
  const l = layouts.current[index];
100
106
  if (!l) return;
101
- if (!placed.current) {
107
+ // Reduce Motion: the indicator jumps to the new segment instead of sliding.
108
+ if (!placed.current || reduce) {
102
109
  lx.setValue(l.x);
103
110
  rx.setValue(l.x + l.width);
104
111
  placed.current = true;
@@ -111,7 +118,7 @@ export function SegmentedControl({
111
118
  Animated.spring(lx, { toValue: l.x, useNativeDriver: false, ...(right ? TRAIL : LEAD) }),
112
119
  Animated.spring(rx, { toValue: l.x + l.width, useNativeDriver: false, ...(right ? LEAD : TRAIL) }),
113
120
  ]).start();
114
- }, [index, measured, lx, rx]);
121
+ }, [index, measured, reduce, lx, rx]);
115
122
 
116
123
  const onSegmentLayout = (i: number) => (e: LayoutChangeEvent) => {
117
124
  const { x: lx, width } = e.nativeEvent.layout;
@@ -253,6 +260,7 @@ export function SegmentedControl({
253
260
  activeFg={activeFg}
254
261
  idleFg={idleFg}
255
262
  disabled={disabled || o.disabled}
263
+ reduce={reduce}
256
264
  />
257
265
  ))}
258
266
  </View>
@@ -271,9 +279,12 @@ interface SegmentProps {
271
279
  activeFg: string;
272
280
  idleFg: string;
273
281
  disabled?: boolean;
282
+ reduce: boolean;
274
283
  }
275
284
 
276
- function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size, height, activeFg, idleFg, disabled }: SegmentProps) {
285
+ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size, height, activeFg, idleFg, disabled, reduce }: SegmentProps) {
286
+ const { font } = useKit();
287
+ const ff = font.family ? { fontFamily: font.family } : null;
277
288
  // a: text colour (JS). press: content squish, pop: little bounce on selection (native, same view).
278
289
  const a = useRef(new Animated.Value(active ? 1 : 0)).current;
279
290
  const press = useRef(new Animated.Value(1)).current;
@@ -281,16 +292,19 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
281
292
  const mounted = useRef(false);
282
293
 
283
294
  useEffect(() => {
284
- Animated.timing(a, { toValue: active ? 1 : 0, duration: 180, useNativeDriver: false }).start();
285
- if (active && mounted.current) {
295
+ Animated.timing(a, { toValue: active ? 1 : 0, duration: reduce ? 150 : 180, useNativeDriver: false }).start();
296
+ // Reduce Motion: no selection pop and no press squish; the label colour still fades.
297
+ if (active && mounted.current && !reduce) {
286
298
  pop.setValue(0.9);
287
299
  Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
288
300
  }
289
301
  mounted.current = true;
290
- }, [active, a, pop]);
302
+ }, [active, reduce, a, pop]);
291
303
 
292
- const pressTo = (to: number) =>
304
+ const pressTo = (to: number) => {
305
+ if (reduce) return press.setValue(1);
293
306
  Animated.spring(press, { toValue: to, useNativeDriver: true, ...(to < 1 ? motion.snappy : motion.bouncy) }).start();
307
+ };
294
308
 
295
309
  const color = a.interpolate({ inputRange: [0, 1], outputRange: [idleFg, activeFg] });
296
310
  const icon =
@@ -310,12 +324,12 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
310
324
  onPressOut={() => pressTo(1)}
311
325
  onLayout={onLayout}
312
326
  hitSlop={height < 44 ? { top: (44 - height) / 2, bottom: (44 - height) / 2 } : undefined}
313
- style={[styles.segment, fullWidth && styles.flex, { height, paddingHorizontal: size.px }]}
327
+ style={[styles.segment, fullWidth && styles.flex, { minHeight: height, paddingHorizontal: size.px }]}
314
328
  >
315
329
  <Animated.View style={[styles.content, { transform: [{ scale: Animated.multiply(press, pop) }] }]}>
316
330
  {icon ? <View style={{ minWidth: size.icon, alignItems: 'center' }}>{icon}</View> : null}
317
331
  {option.label ? (
318
- <Animated.Text numberOfLines={1} style={[styles.label, { fontSize: size.font, color }]}>
332
+ <Animated.Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { fontSize: size.font, color }]}>
319
333
  {option.label}
320
334
  </Animated.Text>
321
335
  ) : null}
@@ -324,6 +338,22 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
324
338
  );
325
339
  }
326
340
 
341
+ function useReduceMotion() {
342
+ const [reduce, setReduce] = useState(false);
343
+ useEffect(() => {
344
+ let alive = true;
345
+ AccessibilityInfo.isReduceMotionEnabled()
346
+ .then((r) => alive && setReduce(r))
347
+ .catch(() => {});
348
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
349
+ return () => {
350
+ alive = false;
351
+ sub.remove();
352
+ };
353
+ }, []);
354
+ return reduce;
355
+ }
356
+
327
357
  const styles = StyleSheet.create({
328
358
  track: { flexDirection: 'row', alignItems: 'center' },
329
359
  flex: { flex: 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
  };