@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, { useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  Animated,
4
5
  Easing,
5
6
  Modal,
@@ -132,6 +133,9 @@ type FlatProps = DatePickerBaseProps & {
132
133
  presets?: boolean | DatePickerPreset<Picked>[];
133
134
  };
134
135
 
136
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
137
+ // content text scales all the way.
138
+ const FONT_CAP = 1.35;
135
139
  const SIZES: Record<DatePickerSize, { h: number; font: number; pad: number; icon: number }> = {
136
140
  sm: { h: 46, font: 15, pad: 12, icon: 17 },
137
141
  md: { h: 56, font: 16, pad: 16, icon: 19 },
@@ -241,6 +245,10 @@ export function DatePicker(props: DatePickerProps) {
241
245
  const restIcon = iconColor ?? colors.mutedForeground;
242
246
  const { width: winW, height: winH } = useWindowDimensions();
243
247
  const ranged = mode === 'range';
248
+ const reduce = useReduceMotion();
249
+ // The pan responder is built once; it reads Reduce Motion through this ref.
250
+ const live = useRef({ reduce });
251
+ live.current = { reduce };
244
252
 
245
253
  const [inner, setInner] = useState<Picked>(defaultValue);
246
254
  const current = value !== undefined ? value : inner;
@@ -251,7 +259,7 @@ export function DatePicker(props: DatePickerProps) {
251
259
  const [draft, setDraft] = useState<Picked>(null);
252
260
  const [calMonth, setCalMonth] = useState(new Date());
253
261
  const [anchor, setAnchor] = useState({ x: 0, y: 0, w: 0, h: 0 });
254
- const fieldRef = useRef<View>(null);
262
+ const fieldRef = useRef<ComponentRef<typeof View>>(null);
255
263
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
256
264
 
257
265
  const ok = !error && !!success;
@@ -269,26 +277,36 @@ export function DatePicker(props: DatePickerProps) {
269
277
  const wide = presentation === 'dialog' || (presentation === 'auto' && winW >= 640);
270
278
 
271
279
  useEffect(() => {
280
+ if (reduce) {
281
+ float.setValue(floating ? 1 : 0);
282
+ return;
283
+ }
272
284
  Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
273
- }, [floating, float]);
285
+ }, [floating, float, reduce]);
274
286
  useEffect(() => {
275
287
  Animated.timing(focus, { toValue: open ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
276
- Animated.spring(halo, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
277
- }, [open, focus, halo]);
288
+ // Reduce Motion: the halo fades in place (its scale interpolation is flat).
289
+ if (reduce) Animated.timing(halo, { toValue: open ? 1 : 0, duration: 150, useNativeDriver: true }).start();
290
+ else Animated.spring(halo, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
291
+ }, [open, focus, halo, reduce]);
278
292
  useEffect(() => {
279
- Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
280
- }, [ok, check]);
293
+ if (reduce) Animated.timing(check, { toValue: ok ? 1 : 0, duration: 150, useNativeDriver: true }).start();
294
+ else Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
295
+ }, [ok, check, reduce]);
281
296
  useEffect(() => {
282
- if (!error) return;
297
+ // Reduce Motion: no shake — the red border and message carry the error.
298
+ if (!error || live.current.reduce) return;
283
299
  shake.setValue(0);
284
300
  Animated.sequence([10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))).start();
285
301
  }, [error, shake]);
286
302
  useEffect(() => {
287
303
  if (open) {
288
304
  drag.setValue(0);
289
- Animated.spring(show, { toValue: 1, useNativeDriver: true, ...(asSheet ? motion.gentle : motion.snappy) }).start();
305
+ // Reduce Motion: the panel fades in place (its translate / scale interpolations are flat).
306
+ if (reduce) Animated.timing(show, { toValue: 1, duration: 150, useNativeDriver: true }).start();
307
+ else Animated.spring(show, { toValue: 1, useNativeDriver: true, ...(asSheet ? motion.gentle : motion.snappy) }).start();
290
308
  } else if (mounted) {
291
- Animated.timing(show, { toValue: 0, duration: motion.fast + 40, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(
309
+ Animated.timing(show, { toValue: 0, duration: reduce ? 150 : motion.fast + 40, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(
292
310
  ({ finished }) => finished && setMounted(false)
293
311
  );
294
312
  }
@@ -395,21 +413,23 @@ export function DatePicker(props: DatePickerProps) {
395
413
  const same = (a: Picked, b: Picked) =>
396
414
  isRange(a) && isRange(b) ? isSameDay(a.start, b.start) && isSameDay(a.end, b.end) : !isRange(a) && !isRange(b) && isSameDay(a, b);
397
415
 
398
- const pan = useMemo(
399
- () =>
400
- PanResponder.create({
401
- onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
402
- onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
403
- onPanResponderRelease: (_, g) => {
404
- if (g.dy > 110 || g.vy > 1.1) {
405
- setOpen(false);
406
- onOpenChange?.(false);
407
- } else Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
408
- },
409
- onPanResponderTerminate: () => Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start(),
410
- }),
411
- [drag, onOpenChange]
412
- );
416
+ const pan = useMemo(() => {
417
+ const settle = () =>
418
+ live.current.reduce
419
+ ? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
420
+ : Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
421
+ return PanResponder.create({
422
+ onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
423
+ onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
424
+ onPanResponderRelease: (_, g) => {
425
+ if (g.dy > 110 || g.vy > 1.1) {
426
+ setOpen(false);
427
+ onOpenChange?.(false);
428
+ } else settle();
429
+ },
430
+ onPanResponderTerminate: settle,
431
+ });
432
+ }, [drag, onOpenChange]);
413
433
 
414
434
  const active = error ? colors.danger : tint;
415
435
  const rest = error ? colors.danger : ok ? colors.success : ((userBorder as string | undefined) ?? colors.input);
@@ -469,7 +489,7 @@ export function DatePicker(props: DatePickerProps) {
469
489
  },
470
490
  ]}
471
491
  >
472
- <Text style={[styles.presetText, ff, { color: on ? tint : colors.foreground }]}>{p.label}</Text>
492
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.presetText, ff, { color: on ? tint : colors.foreground }]}>{p.label}</Text>
473
493
  </Pressable>
474
494
  );
475
495
  })}
@@ -498,14 +518,16 @@ export function DatePicker(props: DatePickerProps) {
498
518
  accessibilityRole="button"
499
519
  style={({ pressed }) => [styles.footBtn, { borderRadius: radius.md, opacity: draft ? (pressed ? 0.6 : 1) : 0.4 }]}
500
520
  >
501
- <Text style={[styles.footText, ff, { color: colors.foreground }]}>Clear</Text>
521
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>
522
+ Clear
523
+ </Text>
502
524
  </Pressable>
503
525
  <Pressable
504
526
  onPress={done}
505
527
  accessibilityRole="button"
506
528
  style={({ pressed }) => [styles.footBtn, styles.footMain, { backgroundColor: tint, borderRadius: radius.md, opacity: pressed ? 0.85 : 1 }]}
507
529
  >
508
- <Text style={[styles.footText, ff, { color: readableOn(tint) }]}>
530
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: readableOn(tint) }]}>
509
531
  {draftDays > 1 ? `Done · ${draftDays} days` : 'Done'}
510
532
  </Text>
511
533
  </Pressable>
@@ -558,13 +580,14 @@ export function DatePicker(props: DatePickerProps) {
558
580
  borderRadius: fieldRadius + 4,
559
581
  borderColor: alpha(active, 0.2),
560
582
  opacity: halo,
561
- transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
583
+ transform: [{ scale: halo.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.97, 1] }) }],
562
584
  },
563
585
  ]}
564
586
  />
565
- <Animated.View style={[styles.field, { height: sz.h, borderRadius: fieldRadius, backgroundColor: bg }, userField, { borderColor }]}>
587
+ <Animated.View style={[styles.field, { minHeight: sz.h, borderRadius: fieldRadius, backgroundColor: bg }, userField, { borderColor }]}>
566
588
  <Text
567
589
  numberOfLines={1}
590
+ maxFontSizeMultiplier={FONT_CAP}
568
591
  style={[
569
592
  styles.value,
570
593
  ff,
@@ -592,7 +615,7 @@ export function DatePicker(props: DatePickerProps) {
592
615
  ]}
593
616
  >
594
617
  {/* The notch: the label paints the surface over the border, so it matches the rendered width. */}
595
- <Animated.Text numberOfLines={1} style={[styles.label, ff, { fontSize: sz.font, backgroundColor: bg }, labelStyle, { color: labelColor }]}>
618
+ <Animated.Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.label, ff, { fontSize: sz.font, backgroundColor: bg }, labelStyle, { color: labelColor }]}>
596
619
  {label}
597
620
  </Animated.Text>
598
621
  </Animated.View>
@@ -603,7 +626,7 @@ export function DatePicker(props: DatePickerProps) {
603
626
  style={{
604
627
  display: ok ? 'flex' : 'none',
605
628
  opacity: check,
606
- transform: [{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) }],
629
+ transform: [{ scale: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.3, 1] }) }],
607
630
  }}
608
631
  >
609
632
  <Icon name="check" size={sz.icon} color={colors.success} />
@@ -623,7 +646,7 @@ export function DatePicker(props: DatePickerProps) {
623
646
  </View>
624
647
  </Animated.View>
625
648
  </Pressable>
626
- {message ? <FieldMessage key={tone + message} text={message} tone={tone} style={messageStyle} /> : null}
649
+ {message ? <FieldMessage key={tone + message} text={message} tone={tone} reduce={reduce} style={messageStyle} /> : null}
627
650
 
628
651
  <Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
629
652
  <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: asSheet ? colors.overlay : 'transparent', opacity: show }]}>
@@ -644,11 +667,15 @@ export function DatePicker(props: DatePickerProps) {
644
667
  ? {
645
668
  opacity: show,
646
669
  transform: [
647
- { scale: show.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] }) },
648
- { translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [16, 0] }), drag) },
670
+ { scale: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.94, 1] }) },
671
+ { translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 16, 0] }), drag) },
649
672
  ],
650
673
  }
651
- : { transform: [{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [winH * 0.85, 0] }), drag) }] },
674
+ : {
675
+ // Reduce Motion: the sheet fades in place instead of sliding up.
676
+ opacity: reduce ? show : 1,
677
+ transform: [{ translateY: Animated.add(show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : winH * 0.85, 0] }), drag) }],
678
+ },
652
679
  ]}
653
680
  >
654
681
  <View {...pan.panHandlers} style={styles.sheetHeader}>
@@ -681,8 +708,8 @@ export function DatePicker(props: DatePickerProps) {
681
708
  {
682
709
  opacity: show,
683
710
  transform: [
684
- { translateY: show.interpolate({ inputRange: [0, 1], outputRange: [below ? -8 : 8, 0] }) },
685
- { scale: show.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) },
711
+ { translateY: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : below ? -8 : 8, 0] }) },
712
+ { scale: show.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.96, 1] }) },
686
713
  ],
687
714
  },
688
715
  below ? { top: anchor.y + anchor.h + 6 } : { bottom: Math.max(12, winH - anchor.y + 6) },
@@ -697,17 +724,28 @@ export function DatePicker(props: DatePickerProps) {
697
724
  }
698
725
 
699
726
  /** Helper / error / success line that slides in; error and success carry an icon. */
700
- function FieldMessage({ text, tone, style }: { text: string; tone: 'error' | 'success' | 'helper'; style?: StyleProp<TextStyle> }) {
727
+ function FieldMessage({
728
+ text,
729
+ tone,
730
+ reduce,
731
+ style,
732
+ }: {
733
+ text: string;
734
+ tone: 'error' | 'success' | 'helper';
735
+ reduce: boolean;
736
+ style?: StyleProp<TextStyle>;
737
+ }) {
701
738
  const { colors, font } = useKit();
702
739
  const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
703
740
  useEffect(() => {
704
- Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
705
- }, [enter]);
741
+ if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
742
+ else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
743
+ }, [enter, reduce]);
706
744
  const color = tone === 'error' ? colors.danger : tone === 'success' ? colors.success : colors.mutedForeground;
707
745
  return (
708
746
  <Animated.View
709
747
  accessibilityLiveRegion="polite"
710
- style={[styles.message, { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }] }]}
748
+ style={[styles.message, { opacity: enter, transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 4, 0] }) }] }]}
711
749
  >
712
750
  {tone !== 'helper' ? <Icon name={tone === 'error' ? 'alert' : 'checkCircle'} size={14} color={color} /> : null}
713
751
  <Text style={[styles.helper, font.family ? { fontFamily: font.family } : null, { color }, style, tone !== 'helper' && { color }]}>{text}</Text>
@@ -747,6 +785,22 @@ function Icon({ name, size, color, strokeWidth = 1.8 }: { name: IconName; size:
747
785
  );
748
786
  }
749
787
 
788
+ function useReduceMotion() {
789
+ const [reduce, setReduce] = useState(false);
790
+ useEffect(() => {
791
+ let alive = true;
792
+ AccessibilityInfo.isReduceMotionEnabled()
793
+ .then((r) => alive && setReduce(r))
794
+ .catch(() => {});
795
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
796
+ return () => {
797
+ alive = false;
798
+ sub.remove();
799
+ };
800
+ }, []);
801
+ return reduce;
802
+ }
803
+
750
804
  const styles = StyleSheet.create({
751
805
  field: { borderWidth: 1.5, justifyContent: 'center' },
752
806
  halo: { position: 'absolute', top: -4, left: -4, right: -4, bottom: -4, borderWidth: 4 },
@@ -775,11 +829,11 @@ const styles = StyleSheet.create({
775
829
  presetScroll: { flexGrow: 0, flexShrink: 0 },
776
830
  calendarScroll: { flexGrow: 0, flexShrink: 1 },
777
831
  presets: { gap: 8, paddingHorizontal: 16, paddingVertical: 8 },
778
- preset: { height: 34, paddingHorizontal: 14, borderWidth: 1, alignItems: 'center', justifyContent: 'center' },
832
+ preset: { minHeight: 34, paddingHorizontal: 14, borderWidth: 1, alignItems: 'center', justifyContent: 'center' },
779
833
  presetText: { fontSize: 13.5, fontWeight: '400' },
780
834
  calendar: { paddingHorizontal: 16, paddingTop: 4, paddingBottom: 6 },
781
835
  footer: { flexDirection: 'row', flexShrink: 0, gap: 8, paddingTop: 12, paddingHorizontal: 16, borderTopWidth: StyleSheet.hairlineWidth },
782
- footBtn: { height: 46, paddingHorizontal: 20, alignItems: 'center', justifyContent: 'center' },
836
+ footBtn: { minHeight: 46, paddingHorizontal: 20, alignItems: 'center', justifyContent: 'center' },
783
837
  footMain: { flex: 1 },
784
838
  footText: { fontSize: 15, fontWeight: '400' },
785
839
  popover: {
@@ -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
  };
@@ -104,6 +104,9 @@ export interface DialogProps {
104
104
  }
105
105
 
106
106
  const SHAKE = [10, -9, 7, -5, 3, 0];
107
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
108
+ // content text scales all the way.
109
+ const FONT_CAP = 1.35;
107
110
 
108
111
  /**
109
112
  * A centred alert dialog. The scrim fades in while the card springs from 0.92 to full size; an
@@ -137,7 +140,8 @@ export function Dialog({
137
140
  accent,
138
141
  style,
139
142
  }: DialogProps) {
140
- const { colors, radius, dark } = useKit();
143
+ const { colors, radius, dark, font } = useKit();
144
+ const ff = font.family ? { fontFamily: font.family } : null;
141
145
  const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
142
146
  const reduceMotion = useReduceMotion();
143
147
  const destructive = variant === 'destructive';
@@ -324,6 +328,7 @@ export function Dialog({
324
328
  maxLength={prompt.maxLength}
325
329
  selectTextOnFocus={prompt.selectTextOnFocus ?? true}
326
330
  autoFocus
331
+ maxFontSizeMultiplier={FONT_CAP}
327
332
  returnKeyType="done"
328
333
  onSubmitEditing={() => run(list[confirmIndex], confirmIndex)}
329
334
  onFocus={() => setFocused(true)}
@@ -331,6 +336,7 @@ export function Dialog({
331
336
  selectionColor={accent ?? colors.accent}
332
337
  style={[
333
338
  ios ? styles.inputIos : styles.input,
339
+ ff,
334
340
  {
335
341
  color: colors.foreground,
336
342
  backgroundColor: alpha(colors.foreground, dark ? 0.07 : 0.04),
@@ -351,19 +357,19 @@ export function Dialog({
351
357
  {title ? (
352
358
  <Text
353
359
  accessibilityRole="header"
354
- style={[ios ? styles.titleIos : styles.title, { color: colors.foreground }, centred && styles.textCenter]}
360
+ style={[ios ? styles.titleIos : styles.title, ff, { color: colors.foreground }, centred && styles.textCenter]}
355
361
  >
356
362
  {title}
357
363
  </Text>
358
364
  ) : null}
359
365
  {description ? (
360
- <Text style={[ios ? styles.descIos : styles.desc, { color: colors.mutedForeground }, centred && styles.textCenter]}>
366
+ <Text style={[ios ? styles.descIos : styles.desc, ff, { color: colors.mutedForeground }, centred && styles.textCenter]}>
361
367
  {description}
362
368
  </Text>
363
369
  ) : null}
364
370
  {field ? <View style={ios ? styles.fieldIos : styles.field}>{field}</View> : null}
365
371
  {error ? (
366
- <Text accessibilityLiveRegion="polite" style={[styles.error, { color: colors.danger }, centred && styles.textCenter]}>
372
+ <Text accessibilityLiveRegion="polite" style={[styles.error, ff, { color: colors.danger }, centred && styles.textCenter]}>
367
373
  {error}
368
374
  </Text>
369
375
  ) : null}
@@ -514,6 +520,8 @@ function PillButton({
514
520
  onPress: () => void;
515
521
  style?: StyleProp<ViewStyle>;
516
522
  }) {
523
+ const { font } = useKit();
524
+ const ff = font.family ? { fontFamily: font.family } : null;
517
525
  const press = useRef(new Animated.Value(0)).current;
518
526
  const to = (v: number) => Animated.spring(press, { toValue: v, useNativeDriver: true, ...motion.snappy }).start();
519
527
  return (
@@ -538,7 +546,7 @@ function PillButton({
538
546
  },
539
547
  ]}
540
548
  >
541
- <Text numberOfLines={1} style={[styles.pillText, { color: fg, opacity: loading ? 0 : 1 }]}>
549
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.pillText, ff, { color: fg, opacity: loading ? 0 : 1 }]}>
542
550
  {label}
543
551
  </Text>
544
552
  {loading ? <ActivityIndicator style={StyleSheet.absoluteFill} size="small" color={fg} /> : null}
@@ -565,6 +573,8 @@ function IosButton({
565
573
  onPress: () => void;
566
574
  style?: StyleProp<ViewStyle>;
567
575
  }) {
576
+ const { font } = useKit();
577
+ const ff = font.family ? { fontFamily: font.family } : null;
568
578
  const hl = useRef(new Animated.Value(0)).current;
569
579
  return (
570
580
  <Pressable
@@ -581,7 +591,7 @@ function IosButton({
581
591
  style={[styles.iosButton, style]}
582
592
  >
583
593
  <Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { backgroundColor: highlight, opacity: hl }]} />
584
- <Text numberOfLines={1} style={[styles.iosText, { color, opacity: loading ? 0 : disabled ? 0.4 : 1 }]}>
594
+ <Text numberOfLines={1} maxFontSizeMultiplier={FONT_CAP} style={[styles.iosText, ff, { color, opacity: loading ? 0 : disabled ? 0.4 : 1 }]}>
585
595
  {label}
586
596
  </Text>
587
597
  {loading ? <ActivityIndicator style={StyleSheet.absoluteFill} size="small" color={color} /> : null}
@@ -744,14 +754,14 @@ const styles = StyleSheet.create({
744
754
  textCenter: { textAlign: 'center' },
745
755
  field: { marginTop: 16 },
746
756
  fieldIos: { marginTop: 14 },
747
- input: { height: 48, paddingHorizontal: 14, fontSize: 16, borderWidth: 1 },
748
- inputIos: { height: 36, paddingHorizontal: 10, fontSize: 14, borderWidth: StyleSheet.hairlineWidth },
757
+ input: { minHeight: 48, paddingHorizontal: 14, fontSize: 16, borderWidth: 1 },
758
+ inputIos: { minHeight: 36, paddingHorizontal: 10, fontSize: 14, borderWidth: StyleSheet.hairlineWidth },
749
759
  error: { fontSize: 13, lineHeight: 18, fontWeight: '400', marginTop: 8 },
750
760
  children: { marginTop: 14 },
751
761
  row: { flexDirection: 'row' },
752
762
  flex: { flex: 1 },
753
763
  actions: { gap: 10, padding: 22, paddingTop: 22 },
754
- pill: { height: 48, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 18 },
764
+ pill: { minHeight: 48, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 18 },
755
765
  pillText: { fontSize: 16, lineHeight: 21, fontWeight: '400', letterSpacing: -0.1 },
756
766
  iosActions: { borderTopWidth: StyleSheet.hairlineWidth },
757
767
  iosButton: { minHeight: 46, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 12 },
@@ -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
  };
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "donut-shop",
3
+ "type": "block",
4
+ "title": "Donut Shop",
5
+ "description": "A dessert shop screen where the products roll. Swipe and the donuts roll across the carousel like wheels; \"+\" grows into a stepper and an add-to-cart bar rises; tap the donut and it zooms out to the edge of the screen, still turning, beside its nutrition. The donuts are drawn — icing, sprinkles, drizzle — with light that stays put while they spin, or pass your own round product art.",
6
+ "tier": "pro",
7
+ "dependencies": [
8
+ "react-native-svg",
9
+ "expo-haptics"
10
+ ],
11
+ "registryDependencies": [
12
+ "kit"
13
+ ],
14
+ "sharedFiles": [
15
+ "lib/kit.tsx"
16
+ ],
17
+ "filesDir": null
18
+ }
@@ -1,5 +1,5 @@
1
1
  import * as Haptics from 'expo-haptics';
2
- import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
2
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
3
3
  import {
4
4
  AccessibilityInfo,
5
5
  Animated,
@@ -38,6 +38,10 @@ const INLINE_BOTTOM = Platform.select({ ios: 18, android: 12, default: 10 }) ??
38
38
  const MODAL_TOP = Platform.select({ ios: 52, android: (StatusBar.currentHeight ?? 24) + 8, default: 16 }) ?? 16;
39
39
  const MODAL_BOTTOM = Platform.select({ ios: 28, android: 16, default: 12 }) ?? 12;
40
40
 
41
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
42
+ // content text scales all the way.
43
+ const FONT_CAP = 1.35;
44
+
41
45
  /** Panel surface hidden past the screen edge, so overshoot and rubber-banding never show a gap. */
42
46
  const EXTRA = 48;
43
47
  /** Extra travel when closed so the panel's shadow is off-screen too. */
@@ -45,7 +49,7 @@ const SHADOW_PAD = 28;
45
49
  const PUSH_SCALE = 0.92;
46
50
  const PUSH_GAP = 14;
47
51
  /** Kit snappy with a touch more damping: the panel lands with a hint of overshoot, not a wobble. */
48
- const OPEN_SPRING = { ...motion.snappy, damping: 22 };
52
+ const OPEN_SPRING = motion.snappy;
49
53
  /** Closing is clamped, so it ends on the first crossing — exits feel faster than entrances. */
50
54
  const CLOSE_SPRING = { ...motion.snappy, overshootClamping: true };
51
55
 
@@ -226,7 +230,7 @@ export function Drawer({
226
230
  }, [isOpen, inline]);
227
231
 
228
232
  // Root position on the page, for the edge-swipe zone (gesture coordinates are page-based).
229
- const rootRef = useRef<View>(null);
233
+ const rootRef = useRef<ComponentRef<typeof View>>(null);
230
234
  const rootX = useRef(0);
231
235
  const measure = useCallback(() => {
232
236
  rootRef.current?.measure((_x, _y, _w, _h, pageX) => {
@@ -492,7 +496,8 @@ export interface DrawerHeaderProps {
492
496
 
493
497
  /** Title block with optional leading node and close button — drop into `header`. */
494
498
  export function DrawerHeader({ title, subtitle, leading, showClose = false, onClose, children, style }: DrawerHeaderProps) {
495
- const { colors, radius, dark } = useKit();
499
+ const { colors, radius, dark, font } = useKit();
500
+ const ff = font.family ? { fontFamily: font.family } : null;
496
501
  const drawerCtx = useContext(DrawerContext);
497
502
  const close = onClose ?? (() => drawerCtx?.close());
498
503
  const canClose = showClose || onClose !== undefined;
@@ -501,12 +506,12 @@ export function DrawerHeader({ title, subtitle, leading, showClose = false, onCl
501
506
  {leading ? <View>{leading}</View> : null}
502
507
  <View style={styles.headerText}>
503
508
  {title ? (
504
- <Text accessibilityRole="header" style={[styles.headerTitle, { color: colors.foreground }]} numberOfLines={1}>
509
+ <Text accessibilityRole="header" style={[styles.headerTitle, ff, { color: colors.foreground }]} numberOfLines={1}>
505
510
  {title}
506
511
  </Text>
507
512
  ) : null}
508
513
  {subtitle ? (
509
- <Text style={[styles.headerSub, { color: colors.mutedForeground }]} numberOfLines={1}>
514
+ <Text style={[styles.headerSub, ff, { color: colors.mutedForeground }]} numberOfLines={1}>
510
515
  {subtitle}
511
516
  </Text>
512
517
  ) : null}
@@ -569,7 +574,8 @@ export function DrawerProfile({
569
574
  accessibilityHint,
570
575
  style,
571
576
  }: DrawerProfileProps) {
572
- const { colors, radius, dark } = useKit();
577
+ const { colors, radius, dark, font } = useKit();
578
+ const ff = font.family ? { fontFamily: font.family } : null;
573
579
  const press = useRef(new Animated.Value(0)).current;
574
580
  const stacked = layout === 'stacked';
575
581
  const trailing =
@@ -587,11 +593,11 @@ export function DrawerProfile({
587
593
  ) : null;
588
594
  const text = (
589
595
  <View style={styles.profileText}>
590
- <Text numberOfLines={1} style={[styles.profileName, { color: colors.foreground }, stacked && styles.profileNameLg]}>
596
+ <Text numberOfLines={1} style={[styles.profileName, ff, { color: colors.foreground }, stacked && styles.profileNameLg]}>
591
597
  {name}
592
598
  </Text>
593
599
  {email ? (
594
- <Text numberOfLines={1} style={[styles.profileEmail, { color: colors.mutedForeground }]}>
600
+ <Text numberOfLines={1} style={[styles.profileEmail, ff, { color: colors.mutedForeground }]}>
595
601
  {email}
596
602
  </Text>
597
603
  ) : null}
@@ -637,7 +643,8 @@ export function DrawerProfile({
637
643
  }
638
644
 
639
645
  function ProfileAvatar({ name, src, size, status }: { name: string; src?: string; size: number; status?: DrawerPresence }) {
640
- const { colors, radius, dark } = useKit();
646
+ const { colors, radius, dark, font } = useKit();
647
+ const ff = font.family ? { fontFamily: font.family } : null;
641
648
  const ring = useContext(SurfaceContext) ?? colors.card;
642
649
  const hue = nameColor(name);
643
650
  const ink = dark ? mix(hue, colors.foreground, 0.35) : mix(hue, colors.foreground, 0.3);
@@ -670,7 +677,7 @@ function ProfileAvatar({ name, src, size, status }: { name: string; src?: string
670
677
  },
671
678
  ]}
672
679
  >
673
- <Text style={{ color: ink, fontSize: size * 0.36, fontWeight: '400', letterSpacing: -0.2 }}>{initials}</Text>
680
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[{ color: ink, fontSize: size * 0.36, fontWeight: '400', letterSpacing: -0.2 }, ff]}>{initials}</Text>
674
681
  {src ? <Image source={{ uri: src }} alt="" style={StyleSheet.absoluteFill} /> : null}
675
682
  </View>
676
683
  {status ? (
@@ -811,7 +818,8 @@ export function DrawerItem({
811
818
  disabled,
812
819
  style,
813
820
  }: DrawerItemProps) {
814
- const { colors, radius, dark } = useKit();
821
+ const { colors, radius, dark, font } = useKit();
822
+ const ff = font.family ? { fontFamily: font.family } : null;
815
823
  const drawer = useContext(DrawerContext);
816
824
  const reduceMotion = useReduceMotion();
817
825
  const danger = tone === 'danger';
@@ -910,7 +918,7 @@ export function DrawerItem({
910
918
  )}
911
919
  </Animated.View>
912
920
  ) : null}
913
- <Text numberOfLines={1} style={[styles.itemLabel, { color: labelColor }]}>
921
+ <Text numberOfLines={1} style={[styles.itemLabel, ff, { color: labelColor }]}>
914
922
  {label}
915
923
  </Text>
916
924
  {showBadge ? (
@@ -921,7 +929,7 @@ export function DrawerItem({
921
929
  { borderRadius: radius.capsule(22), backgroundColor: badgeColors.bg, transform: [{ scale: badgePop }] },
922
930
  ]}
923
931
  >
924
- <Text style={[styles.badgeText, { color: badgeColors.fg }]}>
932
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.badgeText, ff, { color: badgeColors.fg }]}>
925
933
  {typeof badge === 'number' && badge > 99 ? '99+' : badge}
926
934
  </Text>
927
935
  </Animated.View>
@@ -943,12 +951,13 @@ export interface DrawerSectionProps {
943
951
 
944
952
  /** A quiet heading between groups of DrawerItems, with a hairline divider above it. */
945
953
  export function DrawerSection({ title, trailing, divider = true, style }: DrawerSectionProps) {
946
- const { colors, dark } = useKit();
954
+ const { colors, dark, font } = useKit();
955
+ const ff = font.family ? { fontFamily: font.family } : null;
947
956
  return (
948
957
  <View style={style}>
949
958
  {divider ? <View style={[styles.divider, { backgroundColor: alpha(colors.foreground, dark ? 0.1 : 0.07) }]} /> : null}
950
959
  <View style={[styles.section, !divider && styles.sectionFlush]}>
951
- <Text accessibilityRole="header" style={[styles.sectionText, { color: colors.mutedForeground }]}>
960
+ <Text accessibilityRole="header" style={[styles.sectionText, ff, { color: colors.mutedForeground }]}>
952
961
  {title}
953
962
  </Text>
954
963
  {trailing}
@@ -972,7 +981,8 @@ export interface DrawerFooterProps {
972
981
 
973
982
  /** A compact footer row — storage meter, app version, sync status — for the `footer` slot. */
974
983
  export function DrawerFooter({ label, detail, icon, progress, onPress, trailing, style }: DrawerFooterProps) {
975
- const { colors, radius, dark } = useKit();
984
+ const { colors, radius, dark, font } = useKit();
985
+ const ff = font.family ? { fontFamily: font.family } : null;
976
986
  const pct = progress === undefined ? undefined : Math.max(0, Math.min(1, progress));
977
987
  return (
978
988
  <Pressable
@@ -996,7 +1006,7 @@ export function DrawerFooter({ label, detail, icon, progress, onPress, trailing,
996
1006
  </View>
997
1007
  ) : null}
998
1008
  <View style={styles.footerText}>
999
- <Text numberOfLines={1} style={[styles.footerLabel, { color: colors.foreground }]}>
1009
+ <Text numberOfLines={1} style={[styles.footerLabel, ff, { color: colors.foreground }]}>
1000
1010
  {label}
1001
1011
  </Text>
1002
1012
  {pct !== undefined ? (
@@ -1012,7 +1022,7 @@ export function DrawerFooter({ label, detail, icon, progress, onPress, trailing,
1012
1022
  </View>
1013
1023
  ) : null}
1014
1024
  {detail ? (
1015
- <Text numberOfLines={1} style={[styles.footerDetail, { color: colors.mutedForeground }]}>
1025
+ <Text numberOfLines={1} style={[styles.footerDetail, ff, { color: colors.mutedForeground }]}>
1016
1026
  {detail}
1017
1027
  </Text>
1018
1028
  ) : null}
@@ -1111,7 +1121,7 @@ const styles = StyleSheet.create({
1111
1121
  item: { minHeight: ITEM_H, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, gap: 14 },
1112
1122
  itemIcon: { width: 24, alignItems: 'center', justifyContent: 'center' },
1113
1123
  itemLabel: { flex: 1, fontSize: 15.5, fontWeight: '400', letterSpacing: -0.15 },
1114
- badge: { minWidth: 22, height: 22, paddingHorizontal: 7, alignItems: 'center', justifyContent: 'center' },
1124
+ badge: { minWidth: 22, minHeight: 22, paddingHorizontal: 7, alignItems: 'center', justifyContent: 'center' },
1115
1125
  badgePlain: { paddingHorizontal: 2, minWidth: 0 },
1116
1126
  badgeText: { fontSize: 13, fontWeight: '400', fontVariant: ['tabular-nums'] },
1117
1127
  section: {
@@ -1125,7 +1135,7 @@ const styles = StyleSheet.create({
1125
1135
  paddingBottom: 2,
1126
1136
  },
1127
1137
  sectionFlush: { paddingTop: 10 },
1128
- sectionText: { fontSize: 13, lineHeight: 18, fontWeight: '400', letterSpacing: 0.1 },
1138
+ sectionText: { flexShrink: 1, fontSize: 13, lineHeight: 18, fontWeight: '400', letterSpacing: 0.1 },
1129
1139
  divider: { height: StyleSheet.hairlineWidth, marginHorizontal: 14, marginTop: 10, marginBottom: 4 },
1130
1140
  footerRow: { minHeight: 48, flexDirection: 'row', alignItems: 'center', gap: 14, paddingHorizontal: 12, paddingVertical: 8 },
1131
1141
  footerText: { flex: 1, minWidth: 0, gap: 5 },