@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, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
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;
@@ -267,14 +272,16 @@ export function TiltCard({
267
272
  const touching = useRef(false);
268
273
  const origin = useRef({ x: 0, y: 0 });
269
274
  const moved = useRef(false);
270
- const viewRef = useRef<View>(null);
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',
@@ -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,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;
@@ -201,9 +206,11 @@ export function Calendar(props: CalendarProps) {
201
206
  accent,
202
207
  style,
203
208
  } = props as unknown as FlatProps;
204
- const { colors, radius } = useKit();
209
+ const { colors, radius, font } = useKit();
210
+ const ff = font.family ? { fontFamily: font.family } : null;
205
211
  const tint = accent ?? colors.accent;
206
212
  const fmt = useFormatters(locale);
213
+ const reduce = useReduceMotion();
207
214
 
208
215
  const [inner, setInner] = useState<CalendarValue>(defaultValue ?? null);
209
216
  const current = value !== undefined ? value : inner;
@@ -251,7 +258,11 @@ export function Calendar(props: CalendarProps) {
251
258
  setLeaving({ month: prev, dir });
252
259
  slide.stopAnimation();
253
260
  slide.setValue(0);
254
- 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 }) => {
255
266
  if (finished) setLeaving(null);
256
267
  });
257
268
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -266,7 +277,8 @@ export function Calendar(props: CalendarProps) {
266
277
  if (next !== 'days' && view === 'days') setPickYear(visible.getFullYear());
267
278
  setView(next);
268
279
  viewAnim.setValue(0);
269
- 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();
270
282
  };
271
283
 
272
284
  /* selection */
@@ -331,11 +343,13 @@ export function Calendar(props: CalendarProps) {
331
343
  manyKeys: many ? many.map(toDateKey).join('|') : undefined,
332
344
  tint,
333
345
  fullLabel: fmt.full,
346
+ reduce,
334
347
  onDayPress,
335
348
  };
336
349
 
337
350
  const W = width || 320;
338
- const dir = dirRef.current;
351
+ // Reduce Motion: months swap with a fade only, no sideways slide.
352
+ const dir = reduce ? 0 : dirRef.current;
339
353
  const prevDisabled = view === 'days' ? monthIndex(visible) - 1 < minM : view === 'months' ? pickYear * 12 - 1 < minM : false;
340
354
  const nextDisabled = view === 'days' ? monthIndex(visible) + 1 > maxM : view === 'months' ? (pickYear + 1) * 12 > maxM : false;
341
355
  const decadeStart = Math.floor(pickYear / 12) * 12;
@@ -368,6 +382,7 @@ export function Calendar(props: CalendarProps) {
368
382
  numberOfLines={1}
369
383
  style={[
370
384
  styles.title,
385
+ ff,
371
386
  { color: view === 'days' ? colors.foreground : tint },
372
387
  view === 'days'
373
388
  ? {
@@ -379,7 +394,7 @@ export function Calendar(props: CalendarProps) {
379
394
  >
380
395
  {title}
381
396
  </Animated.Text>
382
- {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}
383
398
  </Pressable>
384
399
  <View style={styles.navs}>
385
400
  {showJump ? (
@@ -390,7 +405,9 @@ export function Calendar(props: CalendarProps) {
390
405
  hitSlop={4}
391
406
  style={({ pressed }) => [styles.todayBtn, { borderRadius: radius.capsule(32), backgroundColor: pressed ? colors.secondary : colors.muted }]}
392
407
  >
393
- <Text style={[styles.todayText, { color: tint }]}>Today</Text>
408
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.todayText, ff, { color: tint }]}>
409
+ Today
410
+ </Text>
394
411
  </Pressable>
395
412
  ) : null}
396
413
  <NavButton dir={-1} disabled={prevDisabled} onPress={() => step(-1)} />
@@ -402,12 +419,12 @@ export function Calendar(props: CalendarProps) {
402
419
  <Animated.View
403
420
  style={{
404
421
  opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
405
- 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] }) }],
406
423
  }}
407
424
  >
408
425
  <View style={styles.weekRow} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
409
426
  {weekdays.map((w) => (
410
- <Text key={w.long} numberOfLines={1} style={[styles.weekday, { color: colors.mutedForeground }]}>
427
+ <Text key={w.long} numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.weekday, ff, { color: colors.mutedForeground }]}>
411
428
  {w.short}
412
429
  </Text>
413
430
  ))}
@@ -420,7 +437,7 @@ export function Calendar(props: CalendarProps) {
420
437
  styles.leaving,
421
438
  {
422
439
  opacity: slide.interpolate({ inputRange: [0, 0.7], outputRange: [1, 0], extrapolate: 'clamp' }),
423
- 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] }) }],
424
441
  },
425
442
  ]}
426
443
  >
@@ -444,7 +461,7 @@ export function Calendar(props: CalendarProps) {
444
461
  { minHeight: gridH ? gridH + 28 : undefined },
445
462
  {
446
463
  opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
447
- 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] }) }],
448
465
  },
449
466
  ]}
450
467
  >
@@ -486,7 +503,9 @@ export function Calendar(props: CalendarProps) {
486
503
  },
487
504
  ]}
488
505
  >
489
- <Text style={[styles.pickText, { 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>
490
509
  </View>
491
510
  )}
492
511
  </Pressable>
@@ -518,6 +537,7 @@ interface MonthGridProps {
518
537
  manyKeys?: string;
519
538
  tint: string;
520
539
  fullLabel: (d: Date) => string;
540
+ reduce: boolean;
521
541
  onDayPress: (d: Date) => void;
522
542
  }
523
543
 
@@ -539,6 +559,7 @@ const MonthGrid = memo(function MonthGrid({
539
559
  manyKeys,
540
560
  tint,
541
561
  fullLabel,
562
+ reduce,
542
563
  onDayPress,
543
564
  }: MonthGridProps) {
544
565
  const weeks = useMemo(() => buildWeeks(month, weekStartsOn, fixedWeeks), [month, weekStartsOn, fixedWeeks]);
@@ -593,6 +614,7 @@ const MonthGrid = memo(function MonthGrid({
593
614
  highlight={highlightedDates?.[key]}
594
615
  tile={tiles && inMonth && !inBand}
595
616
  tint={tint}
617
+ reduce={reduce}
596
618
  onPress={onDayPress}
597
619
  />
598
620
  );
@@ -618,6 +640,7 @@ interface DayCellProps {
618
640
  highlight?: string;
619
641
  tile: boolean;
620
642
  tint: string;
643
+ reduce: boolean;
621
644
  onPress: (d: Date) => void;
622
645
  }
623
646
 
@@ -636,15 +659,19 @@ const DayCell = memo(function DayCell({
636
659
  highlight,
637
660
  tile,
638
661
  tint,
662
+ reduce,
639
663
  onPress,
640
664
  }: DayCellProps) {
641
- const { colors, radius } = useKit();
665
+ const { colors, radius, font } = useKit();
666
+ const ff = font.family ? { fontFamily: font.family } : null;
642
667
  const r = tileRadius(radius.md);
643
668
  const pop = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: circle scale + opacity
644
669
  const bandIn = useRef(new Animated.Value(band === 'none' ? 0 : 1)).current; // native: band opacity
645
670
  useEffect(() => {
646
- Animated.spring(pop, { toValue: selected ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
647
- }, [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]);
648
675
  useEffect(() => {
649
676
  Animated.timing(bandIn, { toValue: band === 'none' ? 0 : 1, duration: motion.normal, useNativeDriver: true }).start();
650
677
  }, [band, bandIn]);
@@ -711,14 +738,16 @@ const DayCell = memo(function DayCell({
711
738
  borderRadius: r,
712
739
  backgroundColor: tint,
713
740
  opacity: pop.interpolate({ inputRange: [0, 0.4], outputRange: [0, 1], extrapolate: 'clamp' }),
714
- 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] }) }],
715
742
  },
716
743
  ]}
717
744
  />
718
745
  <View pointerEvents="none" style={styles.label}>
719
746
  <Text
747
+ maxFontSizeMultiplier={FONT_CAP}
720
748
  style={[
721
749
  styles.day,
750
+ ff,
722
751
  { color: textColor },
723
752
  (today || selected) && styles.dayStrong,
724
753
  outside && !selected && styles.outside,
@@ -768,11 +797,15 @@ function NavButton({ dir, disabled, onPress }: { dir: 1 | -1; disabled: boolean;
768
797
  );
769
798
  }
770
799
 
771
- function Chevron({ turned, color }: { turned: boolean; color: string }) {
800
+ function Chevron({ turned, color, reduce }: { turned: boolean; color: string; reduce: boolean }) {
772
801
  const turn = useRef(new Animated.Value(turned ? 1 : 0)).current; // native
773
802
  useEffect(() => {
803
+ if (reduce) {
804
+ turn.setValue(turned ? 1 : 0);
805
+ return;
806
+ }
774
807
  Animated.spring(turn, { toValue: turned ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
775
- }, [turned, turn]);
808
+ }, [turned, turn, reduce]);
776
809
  return (
777
810
  <Animated.View style={{ transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }) }] }}>
778
811
  <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
@@ -782,6 +815,22 @@ function Chevron({ turned, color }: { turned: boolean; color: string }) {
782
815
  );
783
816
  }
784
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
+
785
834
  const styles = StyleSheet.create({
786
835
  root: { alignSelf: 'stretch' },
787
836
  header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, minHeight: 40 },
@@ -789,7 +838,7 @@ const styles = StyleSheet.create({
789
838
  title: { fontSize: 17, fontWeight: '400', letterSpacing: -0.3, flexShrink: 1 },
790
839
  navs: { flexDirection: 'row', alignItems: 'center', gap: 6 },
791
840
  nav: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
792
- todayBtn: { height: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
841
+ todayBtn: { minHeight: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
793
842
  todayText: { fontSize: 13, fontWeight: '400' },
794
843
  weekRow: { flexDirection: 'row', marginBottom: 6 },
795
844
  weekday: { flex: 1, textAlign: 'center', fontSize: 13, fontWeight: '400', letterSpacing: 0.2 },
@@ -817,6 +866,6 @@ const styles = StyleSheet.create({
817
866
  dot: { width: 5, height: 5, borderRadius: 2.5 },
818
867
  pickGrid: { flexDirection: 'row', flexWrap: 'wrap', alignContent: 'center', paddingTop: 4 },
819
868
  pickCell: { width: '33.333%', height: 56, alignItems: 'center', justifyContent: 'center' },
820
- 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 },
821
870
  pickText: { fontSize: 15, fontWeight: '400' },
822
871
  });