@expokit/cli 0.4.0 → 0.4.2

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 (102) hide show
  1. package/README.md +17 -0
  2. package/bin/lib.mjs +1 -1
  3. package/bin/liquidglass.mjs +281 -0
  4. package/package.json +17 -3
  5. package/registry/_docs/accordion.json +1 -1
  6. package/registry/_docs/avatar.json +6 -6
  7. package/registry/_docs/badge.json +3 -3
  8. package/registry/_docs/bottom-nav.json +6 -6
  9. package/registry/_docs/bottom-sheet.json +3 -3
  10. package/registry/_docs/bullet.json +6 -0
  11. package/registry/_docs/button-3d.json +2 -2
  12. package/registry/_docs/button.json +2 -2
  13. package/registry/_docs/calendar.json +5 -5
  14. package/registry/_docs/card.json +1 -1
  15. package/registry/_docs/chip-group.json +1 -1
  16. package/registry/_docs/confetti.json +3 -3
  17. package/registry/_docs/date-picker.json +4 -4
  18. package/registry/_docs/drawer.json +4 -4
  19. package/registry/_docs/flip-card.json +4 -4
  20. package/registry/_docs/floating-input.json +1 -1
  21. package/registry/_docs/grain.json +2 -2
  22. package/registry/_docs/isometric-bar.json +6 -0
  23. package/registry/_docs/liquidglass-tab.json +2 -1
  24. package/registry/_docs/otp-input.json +1 -1
  25. package/registry/_docs/payment-card.json +3 -3
  26. package/registry/_docs/pixel-bar.json +6 -0
  27. package/registry/_docs/polar-area.json +6 -0
  28. package/registry/_docs/progress-steps.json +4 -4
  29. package/registry/_docs/progress.json +2 -2
  30. package/registry/_docs/radio-group.json +1 -1
  31. package/registry/_docs/search-bar.json +3 -3
  32. package/registry/_docs/segmented-control.json +1 -1
  33. package/registry/_docs/select.json +6 -6
  34. package/registry/_docs/skeleton.json +1 -1
  35. package/registry/_docs/slider.json +6 -6
  36. package/registry/_docs/spinner.json +2 -2
  37. package/registry/_docs/stateful-button.json +1 -1
  38. package/registry/_docs/stepper.json +3 -3
  39. package/registry/_docs/tabs.json +2 -2
  40. package/registry/_docs/text-animation.json +4 -4
  41. package/registry/_docs/textarea.json +1 -1
  42. package/registry/_docs/tilt-card.json +3 -3
  43. package/registry/_docs/time-picker.json +6 -6
  44. package/registry/_docs/timeline-chart.json +6 -0
  45. package/registry/_docs/timeline.json +4 -4
  46. package/registry/_docs/treemap.json +6 -0
  47. package/registry/_docs/waterfall.json +6 -0
  48. package/registry/accordion/files/src/components/ui/accordion.tsx +31 -3
  49. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +4 -1
  50. package/registry/alert/files/src/components/ui/alert.tsx +5 -2
  51. package/registry/alert/files/src/components/ui/button.tsx +32 -4
  52. package/registry/badge/files/src/components/ui/badge.tsx +2 -2
  53. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +11 -5
  54. package/registry/button/files/src/components/ui/button.tsx +32 -4
  55. package/registry/button-3d/files/src/components/ui/button-3d.tsx +33 -6
  56. package/registry/calendar/files/src/components/ui/calendar.tsx +62 -17
  57. package/registry/card/files/src/components/ui/card.tsx +25 -5
  58. package/registry/checkbox/files/src/components/ui/checkbox.tsx +26 -4
  59. package/registry/chip-group/files/src/components/ui/chip-group.tsx +35 -5
  60. package/registry/coupon/files/src/components/ui/coupon.tsx +45 -21
  61. package/registry/coupon/files/src/components/ui/flip-card.tsx +22 -3
  62. package/registry/coupon/files/src/components/ui/tilt-card.tsx +57 -16
  63. package/registry/date-picker/files/src/components/ui/calendar.tsx +62 -17
  64. package/registry/date-picker/files/src/components/ui/date-picker.tsx +96 -42
  65. package/registry/dialog/files/src/components/ui/dialog.tsx +9 -5
  66. package/registry/drawer/files/src/components/ui/drawer.tsx +8 -4
  67. package/registry/flip-card/files/src/components/ui/flip-card.tsx +22 -3
  68. package/registry/floating-input/files/src/components/ui/floating-input.tsx +75 -26
  69. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +3 -94
  70. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +2 -1
  71. package/registry/liquidglass-tab/files/src/app/_layout.tsx +2 -2
  72. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -1
  73. package/registry/liquidglass-tab/files/src/components/LiquidGlassTabs.tsx +110 -0
  74. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +31 -6
  75. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +12 -4
  76. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +6 -3
  77. package/registry/liquidglass-tab/files/src/lib/tabs.ts +4 -2
  78. package/registry/list-item/files/src/components/ui/list-item.tsx +10 -3
  79. package/registry/otp-input/files/src/components/ui/otp-input.tsx +50 -19
  80. package/registry/password-input/files/src/components/ui/floating-input.tsx +75 -26
  81. package/registry/password-input/files/src/components/ui/password-input.tsx +53 -19
  82. package/registry/payment-card/files/src/components/ui/flip-card.tsx +22 -3
  83. package/registry/payment-card/files/src/components/ui/payment-card.tsx +19 -15
  84. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +57 -16
  85. package/registry/progress/files/src/components/ui/progress.tsx +9 -2
  86. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +11 -4
  87. package/registry/radio-group/files/src/components/ui/radio-group.tsx +39 -9
  88. package/registry/search-bar/files/src/components/ui/search-bar.tsx +70 -17
  89. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +37 -9
  90. package/registry/select/files/src/components/ui/select.tsx +158 -83
  91. package/registry/slider/files/src/components/ui/slider.tsx +53 -25
  92. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
  93. package/registry/stepper/files/src/components/ui/stepper.tsx +76 -26
  94. package/registry/switch/files/src/components/ui/switch.tsx +56 -19
  95. package/registry/tabs/files/src/components/ui/tabs.tsx +7 -4
  96. package/registry/text-animation/files/src/components/ui/text-animation.tsx +7 -2
  97. package/registry/textarea/files/src/components/ui/textarea.tsx +104 -36
  98. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +57 -16
  99. package/registry/time-picker/files/src/components/ui/time-picker.tsx +154 -73
  100. package/registry/timeline/files/src/components/ui/timeline.tsx +9 -4
  101. package/registry/toast/files/src/components/ui/button.tsx +32 -4
  102. package/registry/toast/files/src/components/ui/toast.tsx +15 -6
@@ -1,5 +1,6 @@
1
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 },
@@ -105,6 +110,7 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
105
110
  ref
106
111
  ) {
107
112
  const { colors, radius, font } = useKit();
113
+ const reduce = useReduceMotion();
108
114
  const ff = font.family ? { fontFamily: font.family } : null;
109
115
  const s = SIZES[size];
110
116
  const tint = accent ?? colors.accent;
@@ -140,28 +146,47 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
140
146
  useEffect(() => {
141
147
  const to = focused ? 1 : 0;
142
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
+ }
143
154
  Animated.spring(halo, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
144
155
  if (focused) {
145
156
  icon.setValue(0.6);
146
157
  Animated.spring(icon, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
147
158
  }
148
- }, [focused, focusJS, halo, icon]);
159
+ }, [focused, focusJS, halo, icon, reduce]);
149
160
 
150
161
  useEffect(() => {
151
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
+ }
152
169
  Animated.spring(cancelJS, { toValue: to, useNativeDriver: false, ...motion.snappy }).start();
153
170
  Animated.spring(cancelN, { toValue: to, useNativeDriver: true, ...motion.snappy }).start();
154
- }, [cancelShown, cancelJS, cancelN]);
171
+ }, [cancelShown, cancelJS, cancelN, reduce]);
155
172
 
156
173
  useEffect(() => {
157
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
+ }
158
181
  Animated.spring(openJS, { toValue: to, useNativeDriver: false, ...motion.gentle }).start();
159
182
  Animated.timing(openN, { toValue: to, duration: open ? motion.normal : motion.fast, delay: open ? 80 : 0, useNativeDriver: true }).start();
160
- }, [open, openJS, openN]);
183
+ }, [open, openJS, openN, reduce]);
161
184
 
162
185
  useEffect(() => {
163
- Animated.spring(clear, { toValue: text && editable && open ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
164
- }, [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]);
165
190
 
166
191
  useEffect(() => {
167
192
  Animated.timing(busy, { toValue: loading ? 1 : 0, duration: motion.fast, useNativeDriver: true }).start();
@@ -230,7 +255,7 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
230
255
  onLayout={(e) => setRowW(e.nativeEvent.layout.width)}
231
256
  style={[styles.row, minimal && { justifyContent: align === 'right' ? 'flex-end' : 'flex-start' }]}
232
257
  >
233
- <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 }]}>
234
259
  <Animated.View
235
260
  pointerEvents="none"
236
261
  style={[
@@ -239,7 +264,7 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
239
264
  borderRadius: r + 4,
240
265
  backgroundColor: alpha(tint, 0.18),
241
266
  opacity: halo,
242
- 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] }) }],
243
268
  },
244
269
  ]}
245
270
  />
@@ -276,6 +301,7 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
276
301
  autoCorrect={false}
277
302
  accessibilityRole="search"
278
303
  accessibilityLabel={props.accessibilityLabel ?? placeholder}
304
+ maxFontSizeMultiplier={FONT_CAP}
279
305
  {...props}
280
306
  autoFocus={autoFocus}
281
307
  editable={editable}
@@ -294,12 +320,12 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
294
320
  if (minimal && !text) setExpanded(false);
295
321
  onBlur?.(e);
296
322
  }}
297
- style={[styles.input, ff, { 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]}
298
324
  />
299
325
  </Animated.View>
300
326
  <Animated.View
301
327
  pointerEvents={text && open ? 'auto' : 'none'}
302
- 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 }] }]}
303
329
  >
304
330
  <Pressable
305
331
  hitSlop={12}
@@ -342,6 +368,7 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
342
368
  {cancelEnabled ? (
343
369
  <>
344
370
  <Text
371
+ maxFontSizeMultiplier={FONT_CAP}
345
372
  pointerEvents="none"
346
373
  onLayout={(e) => setCancelW(Math.ceil(e.nativeEvent.layout.width) + 14)}
347
374
  style={[styles.cancelText, styles.measure, ff, { fontSize: s.font }]}
@@ -357,7 +384,7 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
357
384
  {
358
385
  width: cancelW,
359
386
  opacity: cancelN,
360
- transform: [{ translateX: cancelN.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }],
387
+ transform: [{ translateX: cancelN.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 24, 0] }) }],
361
388
  },
362
389
  ]}
363
390
  >
@@ -367,7 +394,7 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
367
394
  accessibilityLabel={cancelLabel}
368
395
  onPress={cancel}
369
396
  >
370
- <Text numberOfLines={1} style={[styles.cancelText, ff, { color: tint, fontSize: s.font }]}>
397
+ <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={1} style={[styles.cancelText, ff, { color: tint, fontSize: s.font }]}>
371
398
  {cancelLabel}
372
399
  </Text>
373
400
  </Pressable>
@@ -378,24 +405,33 @@ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarPro
378
405
  </View>
379
406
 
380
407
  {suggestions ? (
381
- <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
+ />
382
416
  ) : null}
383
417
  </View>
384
418
  );
385
419
  });
386
420
 
387
- /** 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. */
388
422
  function SuggestionPanel({
389
423
  open,
390
424
  items,
391
425
  query,
392
426
  tint,
427
+ reduce,
393
428
  onSelect,
394
429
  }: {
395
430
  open: boolean;
396
431
  items: SearchSuggestion[];
397
432
  query: string;
398
433
  tint: string;
434
+ reduce: boolean;
399
435
  onSelect: (item: SearchSuggestion) => void;
400
436
  }) {
401
437
  const { colors, radius, font } = useKit();
@@ -408,13 +444,14 @@ function SuggestionPanel({
408
444
  useEffect(() => {
409
445
  if (open) {
410
446
  setMounted(true);
411
- 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();
412
449
  } else {
413
450
  Animated.timing(enter, { toValue: 0, duration: motion.fast, useNativeDriver: true }).start(({ finished }) => {
414
451
  if (finished) setMounted(false);
415
452
  });
416
453
  }
417
- }, [open, enter]);
454
+ }, [open, enter, reduce]);
418
455
 
419
456
  if (!mounted) return null;
420
457
  return (
@@ -428,8 +465,8 @@ function SuggestionPanel({
428
465
  borderRadius: radius.lg,
429
466
  opacity: enter,
430
467
  transform: [
431
- { translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [-6, 0] }) },
432
- { 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] }) },
433
470
  ],
434
471
  },
435
472
  ]}
@@ -498,6 +535,22 @@ function Icon({ name, size, color }: { name: 'search' | 'recent' | 'trending'; s
498
535
  );
499
536
  }
500
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
+
501
554
  const styles = StyleSheet.create({
502
555
  root: { alignSelf: 'stretch' },
503
556
  disabled: { opacity: 0.5 },
@@ -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 },
@@ -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,10 @@ 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) {
277
286
  const { font } = useKit();
278
287
  const ff = font.family ? { fontFamily: font.family } : null;
279
288
  // a: text colour (JS). press: content squish, pop: little bounce on selection (native, same view).
@@ -283,16 +292,19 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
283
292
  const mounted = useRef(false);
284
293
 
285
294
  useEffect(() => {
286
- Animated.timing(a, { toValue: active ? 1 : 0, duration: 180, useNativeDriver: false }).start();
287
- 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) {
288
298
  pop.setValue(0.9);
289
299
  Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start();
290
300
  }
291
301
  mounted.current = true;
292
- }, [active, a, pop]);
302
+ }, [active, reduce, a, pop]);
293
303
 
294
- const pressTo = (to: number) =>
304
+ const pressTo = (to: number) => {
305
+ if (reduce) return press.setValue(1);
295
306
  Animated.spring(press, { toValue: to, useNativeDriver: true, ...(to < 1 ? motion.snappy : motion.bouncy) }).start();
307
+ };
296
308
 
297
309
  const color = a.interpolate({ inputRange: [0, 1], outputRange: [idleFg, activeFg] });
298
310
  const icon =
@@ -312,12 +324,12 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
312
324
  onPressOut={() => pressTo(1)}
313
325
  onLayout={onLayout}
314
326
  hitSlop={height < 44 ? { top: (44 - height) / 2, bottom: (44 - height) / 2 } : undefined}
315
- style={[styles.segment, fullWidth && styles.flex, { height, paddingHorizontal: size.px }]}
327
+ style={[styles.segment, fullWidth && styles.flex, { minHeight: height, paddingHorizontal: size.px }]}
316
328
  >
317
329
  <Animated.View style={[styles.content, { transform: [{ scale: Animated.multiply(press, pop) }] }]}>
318
330
  {icon ? <View style={{ minWidth: size.icon, alignItems: 'center' }}>{icon}</View> : null}
319
331
  {option.label ? (
320
- <Animated.Text numberOfLines={1} style={[styles.label, ff, { fontSize: size.font, color }]}>
332
+ <Animated.Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { fontSize: size.font, color }]}>
321
333
  {option.label}
322
334
  </Animated.Text>
323
335
  ) : null}
@@ -326,6 +338,22 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
326
338
  );
327
339
  }
328
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
+
329
357
  const styles = StyleSheet.create({
330
358
  track: { flexDirection: 'row', alignItems: 'center' },
331
359
  flex: { flex: 1 },