@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
@@ -53,6 +53,9 @@ const RING = {
53
53
  lg: { d: 112, w: 9, font: 24 },
54
54
  };
55
55
  const WHITE = '#ffffff';
56
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
57
+ // content text scales all the way.
58
+ const FONT_CAP = 1.35;
56
59
 
57
60
  const clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));
58
61
 
@@ -104,7 +107,8 @@ export function Progress({
104
107
  striped = false,
105
108
  style,
106
109
  }: ProgressProps) {
107
- const { colors, dark } = useKit();
110
+ const { colors, dark, font } = useKit();
111
+ const ff = font.family ? { fontFamily: font.family } : null;
108
112
  const reduce = useReduceMotion();
109
113
  const top = max ?? (variant === 'steps' ? steps : 100);
110
114
  const unknown = indeterminate || value === undefined;
@@ -137,7 +141,7 @@ export function Progress({
137
141
  done={done}
138
142
  reduce={reduce}
139
143
  />
140
- {label ? <Text style={[styles.ringLabel, { color: colors.mutedForeground }]}>{label}</Text> : null}
144
+ {label ? <Text style={[styles.ringLabel, ff, { color: colors.mutedForeground }]}>{label}</Text> : null}
141
145
  </View>
142
146
  );
143
147
  }
@@ -147,13 +151,13 @@ export function Progress({
147
151
  const header =
148
152
  label || valueShown ? (
149
153
  <View style={styles.header}>
150
- <Text numberOfLines={1} style={[styles.label, { color: colors.foreground }]}>
154
+ <Text numberOfLines={1} style={[styles.label, ff, { color: colors.foreground }]}>
151
155
  {label}
152
156
  </Text>
153
157
  {checkShown ? (
154
158
  <DrawnCheck color={tint} size={18} />
155
159
  ) : valueShown ? (
156
- <Text style={[styles.value, { color: colors.mutedForeground }]}>{text}</Text>
160
+ <Text style={[styles.value, ff, { color: colors.mutedForeground }]}>{text}</Text>
157
161
  ) : null}
158
162
  </View>
159
163
  ) : null;
@@ -685,6 +689,8 @@ function Ring({
685
689
  reduce: boolean;
686
690
  }) {
687
691
  const { d, w, font } = RING[size];
692
+ const kit = useKit();
693
+ const ff = kit.font.family ? { fontFamily: kit.font.family } : null;
688
694
  const id = useId().replace(/[^a-zA-Z0-9]/g, '');
689
695
  const r = (d - w) / 2;
690
696
  const c = 2 * Math.PI * r;
@@ -835,9 +841,13 @@ function Ring({
835
841
  ],
836
842
  }}
837
843
  >
838
- <Text style={[styles.ringValue, { color: fg, fontSize: font }]}>
844
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.ringValue, ff, { color: fg, fontSize: font }]}>
839
845
  {format ? format() : <RingPercent fill={fill} initial={ratio} />}
840
- {format ? null : <Text style={{ fontSize: font * 0.6, color: alpha(fg, 0.55) }}>%</Text>}
846
+ {format ? null : (
847
+ <Text maxFontSizeMultiplier={FONT_CAP} style={{ fontSize: font * 0.6, color: alpha(fg, 0.55) }}>
848
+ %
849
+ </Text>
850
+ )}
841
851
  </Text>
842
852
  </Animated.View>
843
853
  {done ? (
@@ -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
  };
@@ -46,6 +46,9 @@ export interface ProgressStepsProps {
46
46
  style?: StyleProp<ViewStyle>;
47
47
  }
48
48
 
49
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
50
+ // content text scales all the way.
51
+ const FONT_CAP = 1.35;
49
52
  const NODE = 28;
50
53
  const GAP = 6;
51
54
  const LINE = 3;
@@ -70,7 +73,8 @@ export function ProgressSteps({
70
73
  accent,
71
74
  style,
72
75
  }: ProgressStepsProps) {
73
- const { colors, dark } = useKit();
76
+ const { colors, dark, font } = useKit();
77
+ const ff = font.family ? { fontFamily: font.family } : null;
74
78
  const reduce = useReduceMotion();
75
79
  const tint = accent ?? colors.accent;
76
80
  const items = steps.map((s) => (typeof s === 'string' ? { label: s } : s));
@@ -118,7 +122,7 @@ export function ProgressSteps({
118
122
  Animated.timing(pulse, { toValue: 1, duration: 1500, easing: Easing.out(Easing.quad), useNativeDriver: true }),
119
123
  Animated.delay(250),
120
124
  ]),
121
- { resetBeforeIteration: true }
125
+ { iterations: -1, resetBeforeIteration: true }
122
126
  );
123
127
  loop.start();
124
128
  return () => loop.stop();
@@ -146,6 +150,7 @@ export function ProgressSteps({
146
150
  numberOfLines={1}
147
151
  style={[
148
152
  styles.barLabel,
153
+ ff,
149
154
  { color: failed ? colors.danger : colors.foreground },
150
155
  {
151
156
  opacity: labelIn,
@@ -155,7 +160,7 @@ export function ProgressSteps({
155
160
  >
156
161
  {done ? 'All done' : items[shown]!.label}
157
162
  </Animated.Text>
158
- <Text style={[styles.barStep, { color: colors.mutedForeground }]}>
163
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.barStep, ff, { color: colors.mutedForeground }]}>
159
164
  {done ? `${n} of ${n}` : `Step ${shown + 1} of ${n}`}
160
165
  </Text>
161
166
  </View>
@@ -255,7 +260,8 @@ function Step({
255
260
  reduce: boolean;
256
261
  onPress?: () => void;
257
262
  }) {
258
- const { colors } = useKit();
263
+ const { colors, font } = useKit();
264
+ const ff = font.family ? { fontFamily: font.family } : null;
259
265
  const press = useRef(new Animated.Value(0)).current;
260
266
  const pressTo = (v: number) =>
261
267
  Animated.spring(press, { toValue: v, useNativeDriver: true, ...(v ? motion.snappy : motion.bouncy) }).start();
@@ -301,9 +307,9 @@ function Step({
301
307
  {!last ? line(styles.vLine) : null}
302
308
  </View>
303
309
  <View style={[styles.vBody, !last && styles.vBodyGap]}>
304
- <Text style={[styles.vLabel, { color: labelColor }]}>{step.label}</Text>
310
+ <Text style={[styles.vLabel, ff, { color: labelColor }]}>{step.label}</Text>
305
311
  {step.description ? (
306
- <Text style={[styles.vDesc, { color: state === 'error' ? colors.danger : colors.mutedForeground }]}>
312
+ <Text style={[styles.vDesc, ff, { color: state === 'error' ? colors.danger : colors.mutedForeground }]}>
307
313
  {step.description}
308
314
  </Text>
309
315
  ) : null}
@@ -315,11 +321,11 @@ function Step({
315
321
  return (
316
322
  <Wrapper {...a11y} {...pressProps} style={styles.hCol}>
317
323
  <View style={styles.hTop}>{node}</View>
318
- <Text numberOfLines={2} style={[styles.hLabel, { color: labelColor }]}>
324
+ <Text maxFontSizeMultiplier={FONT_CAP} numberOfLines={2} style={[styles.hLabel, ff, { color: labelColor }]}>
319
325
  {step.label}
320
326
  </Text>
321
327
  {step.description ? (
322
- <Text numberOfLines={2} style={[styles.hDesc, { color: colors.mutedForeground }]}>
328
+ <Text numberOfLines={2} style={[styles.hDesc, ff, { color: colors.mutedForeground }]}>
323
329
  {step.description}
324
330
  </Text>
325
331
  ) : null}
@@ -344,7 +350,8 @@ function Node({
344
350
  color: string;
345
351
  reduce: boolean;
346
352
  }) {
347
- const { colors, radius, dark } = useKit();
353
+ const { colors, radius, dark, font } = useKit();
354
+ const ff = font.family ? { fontFamily: font.family } : null;
348
355
  const clamp = 'clamp' as const;
349
356
  const round = { borderRadius: radius.capsule(NODE) };
350
357
  const activeIn = pNative.interpolate({ inputRange: [i - 0.3, i], outputRange: [0, 1], extrapolate: clamp });
@@ -370,7 +377,9 @@ function Node({
370
377
  <View
371
378
  style={[styles.node, round, { borderWidth: 1.5, borderColor: alpha(colors.foreground, dark ? 0.22 : 0.16) }]}
372
379
  >
373
- <Text style={[styles.num, { color: colors.mutedForeground }]}>{i + 1}</Text>
380
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, ff, { color: colors.mutedForeground }]}>
381
+ {i + 1}
382
+ </Text>
374
383
  </View>
375
384
 
376
385
  {/* Current: soft halo, a ring that breathes outwards, accent ring with a tinted core. */}
@@ -411,7 +420,9 @@ function Node({
411
420
  ]}
412
421
  >
413
422
  <View style={[StyleSheet.absoluteFill, round, { backgroundColor: alpha(color, 0.12) }]} />
414
- <Text style={[styles.num, { color }]}>{i + 1}</Text>
423
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.num, ff, { color }]}>
424
+ {i + 1}
425
+ </Text>
415
426
  </Animated.View>
416
427
 
417
428
  {/* Done: solid disc that pops in, then the check draws itself. */}
@@ -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": "qr-code-button",
3
+ "type": "component",
4
+ "title": "QR Code Button",
5
+ "description": "A \"Show QR Code\" pill that swells under your finger and morphs, on a smooth ease, into a white card holding a real, scannable QR code — Copy and close below it. The code is encoded inside the component (byte mode, error correction M, every version up to 40) and drawn as one crisp SVG path, so there's no QR library to install.",
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,6 +1,6 @@
1
1
  import * as Haptics from 'expo-haptics';
2
2
  import React, { useEffect, useRef, useState } from 'react';
3
- import { Animated, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { AccessibilityInfo, Animated, Pressable, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
4
4
  import Svg, { Path } from 'react-native-svg';
5
5
 
6
6
  import { alpha, motion, readableOn, useKit } from '@/lib/kit';
@@ -58,6 +58,7 @@ export function RadioGroup({
58
58
  style,
59
59
  }: RadioGroupProps) {
60
60
  const { colors, radius } = useKit();
61
+ const reduce = useReduceMotion();
61
62
  const tint = accent ?? colors.accent;
62
63
  const [inner, setInner] = useState(defaultValue);
63
64
  const selected = value ?? inner;
@@ -95,6 +96,7 @@ export function RadioGroup({
95
96
  first={i === 0}
96
97
  tint={tint}
97
98
  disabled={disabled || o.disabled}
99
+ reduce={reduce}
98
100
  />
99
101
  ))}
100
102
  </View>
@@ -110,27 +112,36 @@ interface RadioItemProps {
110
112
  first: boolean;
111
113
  tint: string;
112
114
  disabled?: boolean;
115
+ reduce: boolean;
113
116
  }
114
117
 
115
- function RadioItem({ option, active, onPress, variant, horizontal, first, tint, disabled }: RadioItemProps) {
116
- const { colors, radius } = useKit();
118
+ function RadioItem({ option, active, onPress, variant, horizontal, first, tint, disabled, reduce }: RadioItemProps) {
119
+ const { colors, radius, font } = useKit();
120
+ const ff = font.family ? { fontFamily: font.family } : null;
117
121
  // p: colours + svg (JS driver). dot: scale/opacity pops (native). press: press feedback (native).
118
122
  // ripple: a ring that expands and fades when the item gets selected, 0 → 1 (native, rests at 1).
119
123
  const p = useRef(new Animated.Value(active ? 1 : 0)).current;
120
124
  const dot = useRef(new Animated.Value(active ? 1 : 0)).current;
121
125
  const press = useRef(new Animated.Value(1)).current;
122
126
  const ripple = useRef(new Animated.Value(1)).current;
123
- const mounted = useRef(false);
127
+ const was = useRef(active);
124
128
 
125
129
  useEffect(() => {
130
+ const picked = active && !was.current;
131
+ was.current = active;
132
+ // Reduce Motion: colours and the check still fade in, the dot appears at once, no ripple.
133
+ if (reduce) {
134
+ Animated.timing(p, { toValue: active ? 1 : 0, duration: 150, useNativeDriver: false }).start();
135
+ dot.setValue(active ? 1 : 0);
136
+ return;
137
+ }
126
138
  Animated.spring(p, { toValue: active ? 1 : 0, useNativeDriver: false, ...motion.snappy }).start();
127
139
  Animated.spring(dot, { toValue: active ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
128
- if (active && mounted.current) {
140
+ if (picked) {
129
141
  ripple.setValue(0);
130
142
  Animated.timing(ripple, { toValue: 1, duration: 560, useNativeDriver: true }).start();
131
143
  }
132
- mounted.current = true;
133
- }, [active, p, dot, ripple]);
144
+ }, [active, reduce, p, dot, ripple]);
134
145
 
135
146
  const rippleStyle = {
136
147
  backgroundColor: alpha(tint, 0.35),
@@ -138,8 +149,10 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
138
149
  transform: [{ scale: ripple.interpolate({ inputRange: [0, 1], outputRange: [1, 2.3] }) }],
139
150
  };
140
151
 
141
- const pressTo = (to: number) =>
152
+ const pressTo = (to: number) => {
153
+ if (reduce) return press.setValue(1);
142
154
  Animated.spring(press, { toValue: to, useNativeDriver: true, ...(to < 1 ? motion.snappy : motion.bouncy) }).start();
155
+ };
143
156
 
144
157
  const fg = colors.foreground;
145
158
  const muted = colors.mutedForeground;
@@ -154,17 +167,17 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
154
167
 
155
168
  const text = (
156
169
  <View style={styles.text}>
157
- <Text numberOfLines={2} style={[styles.label, { color: fg }]}>
170
+ <Text numberOfLines={2} style={[styles.label, ff, { color: fg }]}>
158
171
  {option.label}
159
172
  </Text>
160
- {option.description ? <Text style={[styles.description, { color: muted }]}>{option.description}</Text> : null}
173
+ {option.description ? <Text style={[styles.description, ff, { color: muted }]}>{option.description}</Text> : null}
161
174
  </View>
162
175
  );
163
176
 
164
177
  if (variant === 'card') {
165
178
  const lit = (from: string, to: string) => p.interpolate({ inputRange: [0, 1], outputRange: [from, to], extrapolate: 'clamp' });
166
179
  const hint = option.hint ? (
167
- <Animated.Text style={[styles.hint, horizontal && styles.hintColumn, { color: lit(fg, tint) }]}>{option.hint}</Animated.Text>
180
+ <Animated.Text style={[styles.hint, ff, horizontal && styles.hintColumn, { color: lit(fg, tint) }]}>{option.hint}</Animated.Text>
168
181
  ) : null;
169
182
  return (
170
183
  <Animated.View style={[horizontal && styles.flex, { opacity: disabled ? 0.5 : 1, transform: [{ scale: press }] }]}>
@@ -191,10 +204,10 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
191
204
  </Animated.View>
192
205
  ) : null}
193
206
  <View style={[styles.text, horizontal && styles.cardTextColumn]}>
194
- <Text numberOfLines={2} style={[styles.label, { color: fg }]}>
207
+ <Text numberOfLines={2} style={[styles.label, ff, { color: fg }]}>
195
208
  {option.label}
196
209
  </Text>
197
- {option.description ? <Text style={[styles.description, { color: muted }]}>{option.description}</Text> : null}
210
+ {option.description ? <Text style={[styles.description, ff, { color: muted }]}>{option.description}</Text> : null}
198
211
  {horizontal ? hint : null}
199
212
  </View>
200
213
  {horizontal ? null : hint}
@@ -241,11 +254,11 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
241
254
  <View style={styles.text}>
242
255
  <Animated.Text
243
256
  numberOfLines={1}
244
- style={[styles.label, { color: p.interpolate({ inputRange: [0, 1], outputRange: [fg, tint], extrapolate: 'clamp' }) }]}
257
+ style={[styles.label, ff, { color: p.interpolate({ inputRange: [0, 1], outputRange: [fg, tint], extrapolate: 'clamp' }) }]}
245
258
  >
246
259
  {option.label}
247
260
  </Animated.Text>
248
- {option.description ? <Text style={[styles.description, { color: muted }]}>{option.description}</Text> : null}
261
+ {option.description ? <Text style={[styles.description, ff, { color: muted }]}>{option.description}</Text> : null}
249
262
  </View>
250
263
  <Svg width={20} height={20} viewBox="0 0 24 24">
251
264
  <AnimatedPath
@@ -286,7 +299,7 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
286
299
  </Animated.View>
287
300
  </Animated.View>
288
301
  {horizontal && !option.description ? (
289
- <Text style={[styles.label, { color: fg }]}>{option.label}</Text>
302
+ <Text style={[styles.label, styles.shrink, ff, { color: fg }]}>{option.label}</Text>
290
303
  ) : (
291
304
  text
292
305
  )}
@@ -294,13 +307,31 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
294
307
  );
295
308
  }
296
309
 
310
+ function useReduceMotion() {
311
+ const [reduce, setReduce] = useState(false);
312
+ useEffect(() => {
313
+ let alive = true;
314
+ AccessibilityInfo.isReduceMotionEnabled()
315
+ .then((r) => alive && setReduce(r))
316
+ .catch(() => {});
317
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
318
+ return () => {
319
+ alive = false;
320
+ sub.remove();
321
+ };
322
+ }, []);
323
+ return reduce;
324
+ }
325
+
297
326
  const styles = StyleSheet.create({
298
327
  vertical: { flexDirection: 'column' },
299
328
  horizontal: { flexDirection: 'row', flexWrap: 'wrap' },
300
329
  flex: { flex: 1, minWidth: 0 },
301
330
  flexGrow: { flexGrow: 1 },
302
331
  row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
303
- rowCompact: { alignItems: 'center' },
332
+ // A long label wraps at a large text size instead of running past the edge.
333
+ rowCompact: { alignItems: 'center', flexShrink: 1 },
334
+ shrink: { flexShrink: 1 },
304
335
  ring: { width: 22, height: 22, borderRadius: 11, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center' },
305
336
  dot: { width: 8, height: 8, borderRadius: 4 },
306
337
  ringSlot: { alignItems: 'center', justifyContent: 'center' },
@@ -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
  };