@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,5 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
- import { Animated, Easing, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
2
+ import { AccessibilityInfo, Animated, Easing, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
  import Svg, { Defs, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
4
4
 
5
5
  import { FlipCard, useFlipProgress } from '@/components/ui/flip-card';
@@ -170,6 +170,10 @@ function contrast(a: string, b: string) {
170
170
  return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
171
171
  }
172
172
 
173
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
174
+ // content text scales all the way.
175
+ const FONT_CAP = 1.35;
176
+
173
177
  const DAY = 86_400_000;
174
178
  const pad2 = (n: number) => String(n).padStart(2, '0');
175
179
 
@@ -197,7 +201,7 @@ function useRemaining(at?: Date | number) {
197
201
  }
198
202
 
199
203
  /** The promo code in a dashed chip; a tap copies it and flips the icon to a check for a moment. */
200
- function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string; color: string; ink: string; fontSize: number; onCopy?: (code: string) => void; labels: Required<CouponLabels> }) {
204
+ function CodeChip({ code, color, ink, fontSize, onCopy, labels, reduce }: { code: string; color: string; ink: string; fontSize: number; onCopy?: (code: string) => void; labels: Required<CouponLabels>; reduce: boolean }) {
201
205
  const { font } = useKit();
202
206
  const ff = font.family ? { fontFamily: font.family } : null;
203
207
  const [copied, setCopied] = useState(false);
@@ -208,8 +212,10 @@ function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string
208
212
  const copy = () => {
209
213
  onCopy?.(code);
210
214
  setCopied(true);
211
- pop.setValue(0.4);
212
- Animated.spring(pop, { toValue: 1, ...motion.bouncy, useNativeDriver: true }).start();
215
+ if (!reduce) {
216
+ pop.setValue(0.4);
217
+ Animated.spring(pop, { toValue: 1, ...motion.bouncy, useNativeDriver: true }).start();
218
+ }
213
219
  clearTimeout(timer.current);
214
220
  timer.current = setTimeout(() => setCopied(false), 1600);
215
221
  };
@@ -228,7 +234,7 @@ function CodeChip({ code, color, ink, fontSize, onCopy, labels }: { code: string
228
234
  <Rect x={0.75} y={0.75} width={box.w - 1.5} height={box.h - 1.5} rx={8} fill="none" stroke={alpha(color, 0.75)} strokeWidth={1.5} strokeDasharray={[5, 4]} />
229
235
  </Svg>
230
236
  ) : null}
231
- <Text numberOfLines={1} style={[{ color: ink, fontSize, fontWeight: '800', letterSpacing: 1, flexShrink: 1 }, ff]}>
237
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[{ color: ink, fontSize, fontWeight: '800', letterSpacing: 1, flexShrink: 1 }, ff]}>
232
238
  {code}
233
239
  </Text>
234
240
  {onCopy ? (
@@ -329,6 +335,7 @@ export function Coupon({
329
335
  }: CouponProps) {
330
336
  const { colors, dark, font } = useKit();
331
337
  const ff = font.family ? { fontFamily: font.family } : null;
338
+ const reduce = useReduceMotion();
332
339
  const theme = typeof themeProp === 'object' ? themeProp : couponThemes[themeProp];
333
340
  const fills = typeof theme.fill === 'string' ? [theme.fill] : theme.fill;
334
341
  const accent = theme.accent ?? fills[fills.length - 1];
@@ -424,7 +431,7 @@ export function Coupon({
424
431
  left !== undefined && left > 0 && !spent ? (
425
432
  <View style={[styles.timer, { backgroundColor: timerBg }]}>
426
433
  <Icon d={ICON.clock} size={11} color={timerInk} />
427
- <Text style={[{ color: timerInk, fontSize: 10.5, fontWeight: '800', fontVariant: ['tabular-nums'] }, ff]}>
434
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: timerInk, fontSize: 10.5, fontWeight: '800', fontVariant: ['tabular-nums'] }, ff]}>
428
435
  {labels.endsIn} {formatCountdown(left)}
429
436
  </Text>
430
437
  </View>
@@ -448,15 +455,15 @@ export function Coupon({
448
455
  // Value panel ------------------------------------------------------------------------------------
449
456
  const valueBlock = (
450
457
  <View style={{ alignItems: 'center' }}>
451
- {prefix ? <Text style={[{ color: valueSoft, fontSize: 10.5, fontWeight: '900', letterSpacing: 1.6 }, ff, lift]}>{prefix.toUpperCase()}</Text> : null}
458
+ {prefix ? <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueSoft, fontSize: 10.5, fontWeight: '900', letterSpacing: 1.6 }, ff, lift]}>{prefix.toUpperCase()}</Text> : null}
452
459
  {locked ? <Icon d={ICON.lock} size={valueSize * 0.6} color={valueInk} /> : null}
453
460
  {/* Vertical coupons set "50% OFF" on one line when it fits, and stack a long unit ("CASHBACK"). */}
454
461
  <View style={{ flexDirection: inline ? 'row' : 'column', alignItems: inline ? 'flex-end' : 'center', gap: inline ? 6 : 0 }}>
455
- <Text numberOfLines={1} adjustsFontSizeToFit style={[{ color: valueInk, fontSize: valueSize, lineHeight: valueSize * 1.08, fontWeight: '900', letterSpacing: -1.2 }, ff, lift]}>
462
+ <Text numberOfLines={1} adjustsFontSizeToFit maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueInk, fontSize: valueSize, lineHeight: valueSize * 1.08, fontWeight: '900', letterSpacing: -1.2 }, ff, lift]}>
456
463
  {value}
457
464
  </Text>
458
465
  {unit ? (
459
- <Text style={[{ color: valueInk, fontSize: horizontal ? 12.5 : 16, fontWeight: '900', letterSpacing: 2.4, marginBottom: inline ? valueSize * 0.14 : 0 }, ff, lift]}>
466
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: valueInk, fontSize: horizontal ? 12.5 : 16, fontWeight: '900', letterSpacing: 2.4, marginBottom: inline ? valueSize * 0.14 : 0 }, ff, lift]}>
460
467
  {unit.toUpperCase()}
461
468
  </Text>
462
469
  ) : null}
@@ -508,13 +515,13 @@ export function Coupon({
508
515
  {
509
516
  marginLeft: 'auto',
510
517
  backgroundColor: applied ? '#16a34a' : locked || spent ? alpha(bodyInk, 0.12) : accent,
511
- transform: [{ scale: pressed ? 0.95 : 1 }],
518
+ transform: [{ scale: pressed && !reduce ? 0.95 : 1 }],
512
519
  paddingHorizontal: size === 'sm' ? 11 : size === 'md' ? 13 : 16,
513
520
  },
514
521
  ]}
515
522
  >
516
523
  {applied || locked ? <Icon d={applied ? ICON.check : ICON.lock} size={13} color={applied ? '#ffffff' : bodySoft} /> : null}
517
- <Text numberOfLines={1} style={[{ color: applied ? '#ffffff' : locked || spent ? bodySoft : readableOn(accent), fontSize: s.chip - 0.5, fontWeight: '800' }, ff]}>
524
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[{ color: applied ? '#ffffff' : locked || spent ? bodySoft : readableOn(accent), fontSize: s.chip - 0.5, fontWeight: '800' }, ff]}>
518
525
  {applied ? labels.applied : locked ? labels.locked : labels.apply}
519
526
  </Text>
520
527
  </Pressable>
@@ -523,11 +530,11 @@ export function Coupon({
523
530
  const footer =
524
531
  expires || canFlip ? (
525
532
  <View style={[styles.row, { justifyContent: 'flex-start', gap: 6 }]}>
526
- {expires ? <Text style={[{ color: bodySoft, fontSize: 11, fontWeight: '600' }, ff]}>{expires}</Text> : null}
533
+ {expires ? <Text style={[{ color: bodySoft, fontSize: 11, fontWeight: '600', flexShrink: 1 }, ff]}>{expires}</Text> : null}
527
534
  {expires && canFlip ? <Text style={[{ color: bodySoft, fontSize: 11 }, ff]}>·</Text> : null}
528
535
  {canFlip ? (
529
536
  <Pressable onPress={toggle} hitSlop={10} accessibilityRole="button" accessibilityLabel={labels.termsTitle}>
530
- <Text style={[{ color: textAccent, fontSize: 11, fontWeight: '800', textDecorationLine: 'underline' }, ff]}>{labels.terms}</Text>
537
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: textAccent, fontSize: 11, fontWeight: '800', textDecorationLine: 'underline' }, ff]}>{labels.terms}</Text>
531
538
  </Pressable>
532
539
  ) : null}
533
540
  </View>
@@ -572,7 +579,7 @@ export function Coupon({
572
579
  // The code never gets cut short: when it and the button don't fit side by side, the button
573
580
  // wraps under it (and stays right-aligned).
574
581
  <View style={[styles.row, { gap: 8, marginTop: 2, flexWrap: 'wrap' }]}>
575
- {code ? <CodeChip code={code} color={textAccent} ink={bodyInk} fontSize={s.chip} onCopy={spent || locked ? undefined : onCopy} labels={labels} /> : <View />}
582
+ {code ? <CodeChip code={code} color={textAccent} ink={bodyInk} fontSize={s.chip} onCopy={spent || locked ? undefined : onCopy} labels={labels} reduce={reduce} /> : <View />}
576
583
  {action}
577
584
  </View>
578
585
  ) : null}
@@ -582,7 +589,8 @@ export function Coupon({
582
589
 
583
590
  const { width: _w, height: _h, ...shape } = outline;
584
591
  const glareClip = measured ? ticketSafeRects(outline) : undefined;
585
- const valueWindows = shine && measured && !spent && !locked
592
+ // The sweep is decorative, so Reduce Motion drops it.
593
+ const valueWindows = shine && !reduce && measured && !spent && !locked
586
594
  ? ticketSafeRects({ width: valueBox.width, height: valueBox.height, corners: valueCorners, edges: valueEdges, edgeSize: v.edgeSize })
587
595
  : null;
588
596
 
@@ -630,23 +638,23 @@ export function Coupon({
630
638
  <TicketShape {...shape} fill={body} style={StyleSheet.absoluteFill}>
631
639
  <Pressable onPress={toggle} accessibilityRole="button" accessibilityHint={labels.back} style={[StyleSheet.absoluteFill, { padding: s.pad + edgePad, gap: 6, flexDirection: horizontal ? 'row' : 'column' }]}>
632
640
  <View style={{ flex: 1, gap: 4 }}>
633
- <Text style={[{ color: bodyInk, fontSize: 12, fontWeight: '800', letterSpacing: 0.3 }, ff]}>{labels.termsTitle}</Text>
641
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodyInk, fontSize: 12, fontWeight: '800', letterSpacing: 0.3 }, ff]}>{labels.termsTitle}</Text>
634
642
  {terms!.slice(0, size === 'lg' ? 5 : 4).map((t, i) => (
635
643
  <View key={i} style={{ flexDirection: 'row', gap: 6 }}>
636
- <Text style={[{ color: textAccent, fontSize: 11, fontWeight: '900' }, ff]}>•</Text>
637
- <Text numberOfLines={2} style={[{ color: bodySoft, fontSize: 11, lineHeight: 14.5, flex: 1 }, ff]}>
644
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: textAccent, fontSize: 11, fontWeight: '900' }, ff]}>•</Text>
645
+ <Text numberOfLines={2} maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodySoft, fontSize: 11, lineHeight: 14.5, flex: 1 }, ff]}>
638
646
  {t}
639
647
  </Text>
640
648
  </View>
641
649
  ))}
642
- <Text style={[{ color: alpha(bodyInk, 0.4), fontSize: 10, fontWeight: '700', marginTop: 'auto' }, ff]}>{labels.back}</Text>
650
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: alpha(bodyInk, 0.4), fontSize: 10, fontWeight: '700', marginTop: 'auto' }, ff]}>{labels.back}</Text>
643
651
  </View>
644
652
  {code ? (
645
653
  <View style={{ alignItems: 'center', justifyContent: 'center', gap: 4 }}>
646
654
  <View style={styles.plate}>
647
655
  <BarcodeLike code={code} width={horizontal ? 74 : 200} height={horizontal ? 54 : 46} />
648
656
  </View>
649
- <Text style={[{ color: bodyInk, fontSize: 10, fontWeight: '800', letterSpacing: 1.2 }, ff]}>{code}</Text>
657
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: bodyInk, fontSize: 10, fontWeight: '800', letterSpacing: 1.2 }, ff]}>{code}</Text>
650
658
  </View>
651
659
  ) : null}
652
660
  </Pressable>
@@ -668,7 +676,7 @@ export function Coupon({
668
676
  return (
669
677
  <Pressable onPress={onPress} disabled={!onPress} accessibilityRole={onPress ? 'button' : undefined} style={style}>
670
678
  {({ pressed }) => (
671
- <View style={{ transform: [{ scale: pressed ? 0.985 : 1 }] }}>
679
+ <View style={{ transform: [{ scale: pressed && !reduce ? 0.985 : 1 }] }}>
672
680
  {/* The plate waits for the painted outline (no black flash on mount), and a dimmed (used /
673
681
  expired) coupon is see-through, so it gets none. */}
674
682
  {spent || !measured ? null : <View pointerEvents="none" style={[styles.shadow, { top: inset, bottom: inset - 4, left: inset, right: inset, borderRadius: v.r, shadowOpacity: dark ? 0.5 : 0.2 }]} />}
@@ -680,6 +688,22 @@ export function Coupon({
680
688
  );
681
689
  }
682
690
 
691
+ function useReduceMotion() {
692
+ const [reduce, setReduce] = useState(false);
693
+ useEffect(() => {
694
+ let alive = true;
695
+ AccessibilityInfo.isReduceMotionEnabled()
696
+ .then((r) => alive && setReduce(r))
697
+ .catch(() => {});
698
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
699
+ return () => {
700
+ alive = false;
701
+ sub.remove();
702
+ };
703
+ }, []);
704
+ return reduce;
705
+ }
706
+
683
707
  const styles = StyleSheet.create({
684
708
  row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
685
709
  center: { alignItems: 'center', justifyContent: 'center' },
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
- import { Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
2
+ import { AccessibilityInfo, Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
 
4
4
  import { motion } from '@/lib/kit';
5
5
 
@@ -48,12 +48,15 @@ const SQUEEZE = {
48
48
  * The flip as an animated 0 → 1 value, springing like FlipCard's own, plus `shadowScaleX` — pass
49
49
  * `progress` to the FlipCard and `shadowScaleX` to a TiltCard wrapped round it, so the tilt
50
50
  * shadow narrows as the card turns edge-on instead of showing as a dark box behind it.
51
+ * With Reduce Motion on, the card turns over at once (no 3D turn).
51
52
  */
52
53
  export function useFlipProgress(flipped: boolean) {
54
+ const reduce = useReduceMotion();
53
55
  const progress = useRef(new Animated.Value(flipped ? 1 : 0)).current;
54
56
  useEffect(() => {
55
- Animated.spring(progress, { toValue: flipped ? 1 : 0, ...motion.gentle, useNativeDriver: true }).start();
56
- }, [flipped, progress]);
57
+ if (reduce) progress.setValue(flipped ? 1 : 0);
58
+ else Animated.spring(progress, { toValue: flipped ? 1 : 0, ...motion.gentle, useNativeDriver: true }).start();
59
+ }, [flipped, progress, reduce]);
57
60
  const shadowScaleX = useMemo(() => progress.interpolate(SQUEEZE), [progress]);
58
61
  return { progress, shadowScaleX };
59
62
  }
@@ -185,6 +188,22 @@ export function FlipCard({
185
188
  );
186
189
  }
187
190
 
191
+ function useReduceMotion() {
192
+ const [reduce, setReduce] = useState(false);
193
+ useEffect(() => {
194
+ let alive = true;
195
+ AccessibilityInfo.isReduceMotionEnabled()
196
+ .then((r) => alive && setReduce(r))
197
+ .catch(() => {});
198
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
199
+ return () => {
200
+ alive = false;
201
+ sub.remove();
202
+ };
203
+ }, []);
204
+ return reduce;
205
+ }
206
+
188
207
  const styles = StyleSheet.create({
189
208
  face: { overflow: 'hidden', backfaceVisibility: 'hidden' },
190
209
  back: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
@@ -1,5 +1,6 @@
1
1
  import React, { createContext, useContext, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  Animated,
4
5
  Easing,
5
6
  Image,
@@ -92,7 +93,8 @@ export interface TiltState {
92
93
  height: number;
93
94
  }
94
95
 
95
- const Ctx = createContext<TiltState | null>(null);
96
+ // `reduce` (Reduce Motion) rides along privately so layers stay flat too; useTilt still returns a TiltState.
97
+ const Ctx = createContext<(TiltState & { reduce: boolean }) | null>(null);
96
98
 
97
99
  /** The surrounding TiltCard's live tilt (null outside one) — for your own layers and effects. */
98
100
  export function useTilt() {
@@ -105,6 +107,8 @@ const PAINT = 96;
105
107
 
106
108
  // Six colour cycles across the paint, so a couple of bands cross the card like real foil. Cool
107
109
  // pinks, violets and cyans stay clean when blended over dark cards (yellow and green turn olive).
110
+ const SMOOTH = Easing.bezier(0.32, 0.72, 0, 1);
111
+
108
112
  const HOLO = Array.from({ length: 37 }, (_, i) => ['#ff6bd6', '#b38cff', '#6fb6ff', '#5ff0ff', '#a7ffe0', '#ffc2ef'][i % 6]);
109
113
 
110
114
  function GlarePaint({ variant }: { variant: 'soft' | 'sharp' }) {
@@ -256,6 +260,7 @@ export function TiltCard({
256
260
  accessibilityLabel,
257
261
  style,
258
262
  }: TiltCardProps) {
263
+ const reduce = useReduceMotion();
259
264
  const [size, setSize] = useState({ w: 0, h: 0 });
260
265
  const sizeRef = useRef(size);
261
266
  sizeRef.current = size;
@@ -270,11 +275,13 @@ export function TiltCard({
270
275
  const viewRef = useRef<ComponentRef<typeof View>>(null);
271
276
  const disabledRef = useRef(disabled);
272
277
  disabledRef.current = disabled;
278
+ const reduceRef = useRef(reduce);
279
+ reduceRef.current = reduce;
273
280
 
274
281
  // Idle sway: a slow figure-eight while nobody is touching.
275
282
  const idleLoop = useRef<Animated.CompositeAnimation | null>(null);
276
283
  const startIdle = () => {
277
- if (!idle) return;
284
+ if (!idle || reduce) return;
278
285
  const ease = Easing.inOut(Easing.sin);
279
286
  const leg = (v: Animated.Value, to: number, ms: number) =>
280
287
  Animated.timing(v, {
@@ -292,10 +299,15 @@ export function TiltCard({
292
299
  idleLoop.current.start();
293
300
  };
294
301
  useEffect(() => {
302
+ // Reduce Motion: no sway; settle where it stopped back at rest.
303
+ if (reduce && !touching.current) {
304
+ tx.setValue(0);
305
+ ty.setValue(0);
306
+ }
295
307
  startIdle();
296
308
  return () => idleLoop.current?.stop();
297
309
  // eslint-disable-next-line react-hooks/exhaustive-deps
298
- }, [idle]);
310
+ }, [idle, reduce]);
299
311
 
300
312
  const aim = (pageX: number, pageY: number) => {
301
313
  const { w, h } = sizeRef.current;
@@ -307,6 +319,12 @@ export function TiltCard({
307
319
  const release = (tapped: boolean) => {
308
320
  touching.current = false;
309
321
  if (tapped && !moved.current) onPressRef.current?.();
322
+ if (reduce) {
323
+ // The card never leaves flat, so only the light has to settle: a short ease, no spring.
324
+ const settle = (v: Animated.Value) => Animated.timing(v, { toValue: 0, duration: 150, easing: SMOOTH, useNativeDriver: true });
325
+ Animated.parallel([settle(tx), settle(ty), settle(press)]).start();
326
+ return;
327
+ }
310
328
  Animated.parallel([
311
329
  Animated.spring(tx, {
312
330
  toValue: 0,
@@ -355,11 +373,15 @@ export function TiltCard({
355
373
  if (typeof px === 'number' && typeof py === 'number') origin.current = { x: px, y: py };
356
374
  });
357
375
  aim(pageX, pageY);
358
- Animated.spring(press, {
359
- toValue: 1,
360
- ...motion.snappy,
361
- useNativeDriver: true,
362
- }).start();
376
+ if (reduceRef.current) {
377
+ Animated.timing(press, { toValue: 1, duration: 150, easing: SMOOTH, useNativeDriver: true }).start();
378
+ } else {
379
+ Animated.spring(press, {
380
+ toValue: 1,
381
+ ...motion.snappy,
382
+ useNativeDriver: true,
383
+ }).start();
384
+ }
363
385
  },
364
386
  onPanResponderMove: (e, g) => {
365
387
  if (Math.abs(g.dx) + Math.abs(g.dy) > 6) moved.current = true;
@@ -380,21 +402,24 @@ export function TiltCard({
380
402
  if (width !== size.w || height !== size.h) setSize({ w: width, h: height });
381
403
  };
382
404
 
405
+ // Reduce Motion keeps the card flat and still: no tilt, lift or parallax. The reflection (light,
406
+ // not movement of the content) still follows the finger.
407
+ const tilt = reduce ? 0 : maxTilt;
383
408
  const rotateY = tx.interpolate({
384
409
  inputRange: [-1, 1],
385
- outputRange: [`${-maxTilt}deg`, `${maxTilt}deg`],
410
+ outputRange: [`${-tilt}deg`, `${tilt}deg`],
386
411
  });
387
412
  const rotateX = ty.interpolate({
388
413
  inputRange: [-1, 1],
389
- outputRange: [`${maxTilt}deg`, `${-maxTilt}deg`],
414
+ outputRange: [`${tilt}deg`, `${-tilt}deg`],
390
415
  });
391
416
  const scale = press.interpolate({
392
417
  inputRange: [0, 1],
393
- outputRange: [1, pressScale],
418
+ outputRange: [1, reduce ? 1 : pressScale],
394
419
  });
395
420
 
396
- const ctx = useMemo(() => ({ tx, ty, press, width: size.w, height: size.h }), [tx, ty, press, size.w, size.h]);
397
- const drift = Math.min(10, size.w * 0.03);
421
+ const ctx = useMemo(() => ({ tx, ty, press, width: size.w, height: size.h, reduce }), [tx, ty, press, size.w, size.h, reduce]);
422
+ const drift = reduce ? 0 : Math.min(10, size.w * 0.03);
398
423
  // The plate sits well inside the outline — far enough that it never shows through a notch or a
399
424
  // see-through corner, even at full tilt; the shadow offset does the downward shift.
400
425
  const body = shadowRect ?? { x: 0, y: 0, width: size.w, height: size.h };
@@ -433,7 +458,7 @@ export function TiltCard({
433
458
  {
434
459
  scale: press.interpolate({
435
460
  inputRange: [0, 1],
436
- outputRange: [1, 1.035],
461
+ outputRange: [1, reduce ? 1 : 1.035],
437
462
  }),
438
463
  },
439
464
  { scaleX: shadowScaleX ?? 1 },
@@ -516,7 +541,7 @@ export function TiltCard({
516
541
  export function TiltLayer({ children, depth = 0.5, style }: TiltLayerProps) {
517
542
  const ctx = useContext(Ctx);
518
543
  if (!ctx) throw new Error('TiltLayer must be inside TiltCard');
519
- const d = depth * 14;
544
+ const d = ctx.reduce ? 0 : depth * 14;
520
545
  return (
521
546
  <Animated.View
522
547
  pointerEvents="box-none"
@@ -539,7 +564,7 @@ export function TiltLayer({ children, depth = 0.5, style }: TiltLayerProps) {
539
564
  {
540
565
  scale: ctx.press.interpolate({
541
566
  inputRange: [0, 1],
542
- outputRange: [1, 1 + Math.max(0, depth) * 0.035],
567
+ outputRange: [1, ctx.reduce ? 1 : 1 + Math.max(0, depth) * 0.035],
543
568
  }),
544
569
  },
545
570
  ],
@@ -616,6 +641,22 @@ export function TiltFoil({ children, radius = 8, base = '#d7dbe2', style }: Tilt
616
641
  );
617
642
  }
618
643
 
644
+ function useReduceMotion() {
645
+ const [reduce, setReduce] = useState(false);
646
+ useEffect(() => {
647
+ let alive = true;
648
+ AccessibilityInfo.isReduceMotionEnabled()
649
+ .then((r) => alive && setReduce(r))
650
+ .catch(() => {});
651
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
652
+ return () => {
653
+ alive = false;
654
+ sub.remove();
655
+ };
656
+ }, []);
657
+ return reduce;
658
+ }
659
+
619
660
  const styles = StyleSheet.create({
620
661
  plate: {
621
662
  position: 'absolute',
@@ -1,6 +1,8 @@
1
1
  import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  Animated,
5
+ Easing,
4
6
  PanResponder,
5
7
  Pressable,
6
8
  StyleSheet,
@@ -98,6 +100,9 @@ type FlatProps = CalendarBaseProps & {
98
100
  onChange?: (value: CalendarValue) => void;
99
101
  };
100
102
 
103
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
104
+ // content text scales all the way.
105
+ const FONT_CAP = 1.35;
101
106
  const CELL_H = 48;
102
107
  const CIRCLE = 38;
103
108
  const CIRCLE_TOP = 2;
@@ -205,6 +210,7 @@ export function Calendar(props: CalendarProps) {
205
210
  const ff = font.family ? { fontFamily: font.family } : null;
206
211
  const tint = accent ?? colors.accent;
207
212
  const fmt = useFormatters(locale);
213
+ const reduce = useReduceMotion();
208
214
 
209
215
  const [inner, setInner] = useState<CalendarValue>(defaultValue ?? null);
210
216
  const current = value !== undefined ? value : inner;
@@ -252,7 +258,11 @@ export function Calendar(props: CalendarProps) {
252
258
  setLeaving({ month: prev, dir });
253
259
  slide.stopAnimation();
254
260
  slide.setValue(0);
255
- Animated.spring(slide, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start(({ finished }) => {
261
+ // Reduce Motion: a short cross-fade in place (the translate interpolations are flat).
262
+ const anim = reduce
263
+ ? Animated.timing(slide, { toValue: 1, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true })
264
+ : Animated.spring(slide, { toValue: 1, useNativeDriver: true, ...motion.gentle });
265
+ anim.start(({ finished }) => {
256
266
  if (finished) setLeaving(null);
257
267
  });
258
268
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -267,7 +277,8 @@ export function Calendar(props: CalendarProps) {
267
277
  if (next !== 'days' && view === 'days') setPickYear(visible.getFullYear());
268
278
  setView(next);
269
279
  viewAnim.setValue(0);
270
- Animated.spring(viewAnim, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
280
+ if (reduce) Animated.timing(viewAnim, { toValue: 1, duration: 150, useNativeDriver: true }).start();
281
+ else Animated.spring(viewAnim, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
271
282
  };
272
283
 
273
284
  /* selection */
@@ -332,11 +343,13 @@ export function Calendar(props: CalendarProps) {
332
343
  manyKeys: many ? many.map(toDateKey).join('|') : undefined,
333
344
  tint,
334
345
  fullLabel: fmt.full,
346
+ reduce,
335
347
  onDayPress,
336
348
  };
337
349
 
338
350
  const W = width || 320;
339
- const dir = dirRef.current;
351
+ // Reduce Motion: months swap with a fade only, no sideways slide.
352
+ const dir = reduce ? 0 : dirRef.current;
340
353
  const prevDisabled = view === 'days' ? monthIndex(visible) - 1 < minM : view === 'months' ? pickYear * 12 - 1 < minM : false;
341
354
  const nextDisabled = view === 'days' ? monthIndex(visible) + 1 > maxM : view === 'months' ? (pickYear + 1) * 12 > maxM : false;
342
355
  const decadeStart = Math.floor(pickYear / 12) * 12;
@@ -381,7 +394,7 @@ export function Calendar(props: CalendarProps) {
381
394
  >
382
395
  {title}
383
396
  </Animated.Text>
384
- {monthPicker ? <Chevron turned={view !== 'days'} color={view === 'days' ? colors.mutedForeground : tint} /> : null}
397
+ {monthPicker ? <Chevron turned={view !== 'days'} color={view === 'days' ? colors.mutedForeground : tint} reduce={reduce} /> : null}
385
398
  </Pressable>
386
399
  <View style={styles.navs}>
387
400
  {showJump ? (
@@ -392,7 +405,9 @@ export function Calendar(props: CalendarProps) {
392
405
  hitSlop={4}
393
406
  style={({ pressed }) => [styles.todayBtn, { borderRadius: radius.capsule(32), backgroundColor: pressed ? colors.secondary : colors.muted }]}
394
407
  >
395
- <Text style={[styles.todayText, ff, { color: tint }]}>Today</Text>
408
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.todayText, ff, { color: tint }]}>
409
+ Today
410
+ </Text>
396
411
  </Pressable>
397
412
  ) : null}
398
413
  <NavButton dir={-1} disabled={prevDisabled} onPress={() => step(-1)} />
@@ -404,12 +419,12 @@ export function Calendar(props: CalendarProps) {
404
419
  <Animated.View
405
420
  style={{
406
421
  opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
407
- transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [1.04, 1] }) }],
422
+ transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 1.04, 1] }) }],
408
423
  }}
409
424
  >
410
425
  <View style={styles.weekRow} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
411
426
  {weekdays.map((w) => (
412
- <Text key={w.long} numberOfLines={1} style={[styles.weekday, ff, { color: colors.mutedForeground }]}>
427
+ <Text key={w.long} numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.weekday, ff, { color: colors.mutedForeground }]}>
413
428
  {w.short}
414
429
  </Text>
415
430
  ))}
@@ -422,7 +437,7 @@ export function Calendar(props: CalendarProps) {
422
437
  styles.leaving,
423
438
  {
424
439
  opacity: slide.interpolate({ inputRange: [0, 0.7], outputRange: [1, 0], extrapolate: 'clamp' }),
425
- transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [0, -leaving.dir * W * 0.6] }) }],
440
+ transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [0, reduce ? 0 : -leaving.dir * W * 0.6] }) }],
426
441
  },
427
442
  ]}
428
443
  >
@@ -446,7 +461,7 @@ export function Calendar(props: CalendarProps) {
446
461
  { minHeight: gridH ? gridH + 28 : undefined },
447
462
  {
448
463
  opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
449
- transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] }) }],
464
+ transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.94, 1] }) }],
450
465
  },
451
466
  ]}
452
467
  >
@@ -488,7 +503,9 @@ export function Calendar(props: CalendarProps) {
488
503
  },
489
504
  ]}
490
505
  >
491
- <Text style={[styles.pickText, ff, { color: selected ? readableOn(tint) : isNow ? tint : colors.foreground }]}>{label}</Text>
506
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.pickText, ff, { color: selected ? readableOn(tint) : isNow ? tint : colors.foreground }]}>
507
+ {label}
508
+ </Text>
492
509
  </View>
493
510
  )}
494
511
  </Pressable>
@@ -520,6 +537,7 @@ interface MonthGridProps {
520
537
  manyKeys?: string;
521
538
  tint: string;
522
539
  fullLabel: (d: Date) => string;
540
+ reduce: boolean;
523
541
  onDayPress: (d: Date) => void;
524
542
  }
525
543
 
@@ -541,6 +559,7 @@ const MonthGrid = memo(function MonthGrid({
541
559
  manyKeys,
542
560
  tint,
543
561
  fullLabel,
562
+ reduce,
544
563
  onDayPress,
545
564
  }: MonthGridProps) {
546
565
  const weeks = useMemo(() => buildWeeks(month, weekStartsOn, fixedWeeks), [month, weekStartsOn, fixedWeeks]);
@@ -595,6 +614,7 @@ const MonthGrid = memo(function MonthGrid({
595
614
  highlight={highlightedDates?.[key]}
596
615
  tile={tiles && inMonth && !inBand}
597
616
  tint={tint}
617
+ reduce={reduce}
598
618
  onPress={onDayPress}
599
619
  />
600
620
  );
@@ -620,6 +640,7 @@ interface DayCellProps {
620
640
  highlight?: string;
621
641
  tile: boolean;
622
642
  tint: string;
643
+ reduce: boolean;
623
644
  onPress: (d: Date) => void;
624
645
  }
625
646
 
@@ -638,6 +659,7 @@ const DayCell = memo(function DayCell({
638
659
  highlight,
639
660
  tile,
640
661
  tint,
662
+ reduce,
641
663
  onPress,
642
664
  }: DayCellProps) {
643
665
  const { colors, radius, font } = useKit();
@@ -646,8 +668,10 @@ const DayCell = memo(function DayCell({
646
668
  const pop = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: circle scale + opacity
647
669
  const bandIn = useRef(new Animated.Value(band === 'none' ? 0 : 1)).current; // native: band opacity
648
670
  useEffect(() => {
649
- Animated.spring(pop, { toValue: selected ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
650
- }, [selected, pop]);
671
+ // Reduce Motion: the selection fills in with a quick fade, no scale pop.
672
+ if (reduce) Animated.timing(pop, { toValue: selected ? 1 : 0, duration: 150, useNativeDriver: true }).start();
673
+ else Animated.spring(pop, { toValue: selected ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
674
+ }, [selected, pop, reduce]);
651
675
  useEffect(() => {
652
676
  Animated.timing(bandIn, { toValue: band === 'none' ? 0 : 1, duration: motion.normal, useNativeDriver: true }).start();
653
677
  }, [band, bandIn]);
@@ -714,12 +738,13 @@ const DayCell = memo(function DayCell({
714
738
  borderRadius: r,
715
739
  backgroundColor: tint,
716
740
  opacity: pop.interpolate({ inputRange: [0, 0.4], outputRange: [0, 1], extrapolate: 'clamp' }),
717
- transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [0.45, 1] }) }],
741
+ transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.45, 1] }) }],
718
742
  },
719
743
  ]}
720
744
  />
721
745
  <View pointerEvents="none" style={styles.label}>
722
746
  <Text
747
+ maxFontSizeMultiplier={FONT_CAP}
723
748
  style={[
724
749
  styles.day,
725
750
  ff,
@@ -772,11 +797,15 @@ function NavButton({ dir, disabled, onPress }: { dir: 1 | -1; disabled: boolean;
772
797
  );
773
798
  }
774
799
 
775
- function Chevron({ turned, color }: { turned: boolean; color: string }) {
800
+ function Chevron({ turned, color, reduce }: { turned: boolean; color: string; reduce: boolean }) {
776
801
  const turn = useRef(new Animated.Value(turned ? 1 : 0)).current; // native
777
802
  useEffect(() => {
803
+ if (reduce) {
804
+ turn.setValue(turned ? 1 : 0);
805
+ return;
806
+ }
778
807
  Animated.spring(turn, { toValue: turned ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
779
- }, [turned, turn]);
808
+ }, [turned, turn, reduce]);
780
809
  return (
781
810
  <Animated.View style={{ transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }) }] }}>
782
811
  <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
@@ -786,6 +815,22 @@ function Chevron({ turned, color }: { turned: boolean; color: string }) {
786
815
  );
787
816
  }
788
817
 
818
+ function useReduceMotion() {
819
+ const [reduce, setReduce] = useState(false);
820
+ useEffect(() => {
821
+ let alive = true;
822
+ AccessibilityInfo.isReduceMotionEnabled()
823
+ .then((r) => alive && setReduce(r))
824
+ .catch(() => {});
825
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
826
+ return () => {
827
+ alive = false;
828
+ sub.remove();
829
+ };
830
+ }, []);
831
+ return reduce;
832
+ }
833
+
789
834
  const styles = StyleSheet.create({
790
835
  root: { alignSelf: 'stretch' },
791
836
  header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, minHeight: 40 },
@@ -793,7 +838,7 @@ const styles = StyleSheet.create({
793
838
  title: { fontSize: 17, fontWeight: '400', letterSpacing: -0.3, flexShrink: 1 },
794
839
  navs: { flexDirection: 'row', alignItems: 'center', gap: 6 },
795
840
  nav: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
796
- todayBtn: { height: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
841
+ todayBtn: { minHeight: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
797
842
  todayText: { fontSize: 13, fontWeight: '400' },
798
843
  weekRow: { flexDirection: 'row', marginBottom: 6 },
799
844
  weekday: { flex: 1, textAlign: 'center', fontSize: 13, fontWeight: '400', letterSpacing: 0.2 },
@@ -821,6 +866,6 @@ const styles = StyleSheet.create({
821
866
  dot: { width: 5, height: 5, borderRadius: 2.5 },
822
867
  pickGrid: { flexDirection: 'row', flexWrap: 'wrap', alignContent: 'center', paddingTop: 4 },
823
868
  pickCell: { width: '33.333%', height: 56, alignItems: 'center', justifyContent: 'center' },
824
- pickInner: { minWidth: 76, height: 40, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5 },
869
+ pickInner: { minWidth: 76, minHeight: 40, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5 },
825
870
  pickText: { fontSize: 15, fontWeight: '400' },
826
871
  });