@expokit/cli 0.2.0 → 0.4.0

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 (232) hide show
  1. package/README.md +20 -2
  2. package/bin/expo-kit.js +219 -44
  3. package/bin/lib.mjs +110 -1
  4. package/bin/mcp.mjs +48 -19
  5. package/package.json +9 -4
  6. package/registry/_docs/accordion.json +16 -0
  7. package/registry/_docs/action-sheet.json +28 -0
  8. package/registry/_docs/alert.json +28 -0
  9. package/registry/_docs/assistant-app.json +47 -0
  10. package/registry/_docs/avatar.json +25 -1
  11. package/registry/_docs/badge.json +36 -5
  12. package/registry/_docs/bottom-nav.json +30 -6
  13. package/registry/_docs/bottom-sheet.json +28 -0
  14. package/registry/_docs/bullet.json +3 -0
  15. package/registry/_docs/button-3d.json +18 -2
  16. package/registry/_docs/button.json +16 -0
  17. package/registry/_docs/calendar.json +21 -5
  18. package/registry/_docs/card-stack.json +16 -0
  19. package/registry/_docs/card.json +20 -0
  20. package/registry/_docs/carousel.json +20 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +16 -0
  23. package/registry/_docs/checkout.json +10 -1
  24. package/registry/_docs/chip-group.json +16 -0
  25. package/registry/_docs/confetti.json +23 -3
  26. package/registry/_docs/coupon.json +16 -0
  27. package/registry/_docs/cube-pager.json +16 -0
  28. package/registry/_docs/dashboard.json +3 -0
  29. package/registry/_docs/date-picker.json +24 -4
  30. package/registry/_docs/dialog.json +24 -0
  31. package/registry/_docs/donut-shop.json +88 -0
  32. package/registry/_docs/drawer.json +32 -4
  33. package/registry/_docs/fab.json +24 -0
  34. package/registry/_docs/fintech-app.json +49 -0
  35. package/registry/_docs/flip-card.json +16 -0
  36. package/registry/_docs/floating-input.json +25 -5
  37. package/registry/_docs/food-app.json +47 -0
  38. package/registry/_docs/gift-card.json +16 -0
  39. package/registry/_docs/glass-button.json +24 -0
  40. package/registry/_docs/glass-nav-bar.json +24 -0
  41. package/registry/_docs/glass-search-bar.json +24 -0
  42. package/registry/_docs/glass-segmented.json +28 -0
  43. package/registry/_docs/glass-sheet.json +28 -0
  44. package/registry/_docs/glass-tab-bar.json +24 -0
  45. package/registry/_docs/glass.json +17 -1
  46. package/registry/_docs/grain.json +80 -0
  47. package/registry/_docs/inbox.json +3 -0
  48. package/registry/_docs/isometric-bar.json +3 -0
  49. package/registry/_docs/kit.json +3 -0
  50. package/registry/_docs/liquidglass-tab.json +3 -0
  51. package/registry/_docs/list-item.json +16 -0
  52. package/registry/_docs/loyalty-card.json +16 -0
  53. package/registry/_docs/morphing-button-group.json +96 -0
  54. package/registry/_docs/number-ticker.json +16 -0
  55. package/registry/_docs/offers.json +3 -0
  56. package/registry/_docs/onboarding.json +3 -0
  57. package/registry/_docs/orb-reveal.json +132 -0
  58. package/registry/_docs/otp-input.json +28 -4
  59. package/registry/_docs/parallax-layers.json +20 -0
  60. package/registry/_docs/parallax-scroll.json +24 -0
  61. package/registry/_docs/password-input.json +23 -3
  62. package/registry/_docs/payment-card.json +16 -0
  63. package/registry/_docs/pixel-bar.json +3 -0
  64. package/registry/_docs/polar-area.json +3 -0
  65. package/registry/_docs/profile.json +3 -0
  66. package/registry/_docs/progress-steps.json +24 -0
  67. package/registry/_docs/progress.json +20 -0
  68. package/registry/_docs/qr-code-button.json +122 -0
  69. package/registry/_docs/radio-group.json +20 -0
  70. package/registry/_docs/rating.json +24 -0
  71. package/registry/_docs/ring-carousel.json +20 -0
  72. package/registry/_docs/scratch-card.json +20 -0
  73. package/registry/_docs/search-bar.json +23 -3
  74. package/registry/_docs/search-results.json +3 -0
  75. package/registry/_docs/segmented-control.json +20 -0
  76. package/registry/_docs/select.json +33 -8
  77. package/registry/_docs/settings.json +9 -0
  78. package/registry/_docs/sign-in.json +3 -0
  79. package/registry/_docs/skeleton.json +22 -2
  80. package/registry/_docs/slide-to-confirm.json +24 -0
  81. package/registry/_docs/slider.json +27 -7
  82. package/registry/_docs/spin-wheel.json +24 -0
  83. package/registry/_docs/spinner.json +41 -11
  84. package/registry/_docs/stateful-button.json +184 -0
  85. package/registry/_docs/stepper.json +23 -7
  86. package/registry/_docs/swipe-deck.json +16 -0
  87. package/registry/_docs/swipeable-row.json +24 -1
  88. package/registry/_docs/switch.json +20 -0
  89. package/registry/_docs/tabs.json +24 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +18 -2
  92. package/registry/_docs/ticket-card.json +20 -0
  93. package/registry/_docs/ticket-shape.json +4 -1
  94. package/registry/_docs/tilt-card.json +20 -4
  95. package/registry/_docs/time-picker.json +22 -6
  96. package/registry/_docs/timeline-chart.json +3 -0
  97. package/registry/_docs/timeline.json +18 -2
  98. package/registry/_docs/toast.json +32 -8
  99. package/registry/_docs/treemap.json +3 -0
  100. package/registry/_docs/waterfall.json +3 -0
  101. package/registry/_docs/wheel-list.json +20 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +5 -4
  103. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  104. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +9 -8
  105. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  106. package/registry/alert/files/src/components/ui/alert.tsx +7 -4
  107. package/registry/alert/files/src/components/ui/button.tsx +4 -3
  108. package/registry/alert/files/src/lib/kit.tsx +8 -5
  109. package/registry/assistant-app.json +49 -0
  110. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  111. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  112. package/registry/badge/files/src/components/ui/badge.tsx +345 -21
  113. package/registry/badge/files/src/lib/kit.tsx +8 -5
  114. package/registry/badge.json +1 -1
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +14 -7
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  117. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  118. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  119. package/registry/button/files/src/components/ui/button.tsx +4 -3
  120. package/registry/button/files/src/lib/kit.tsx +8 -5
  121. package/registry/button-3d/files/src/components/ui/button-3d.tsx +3 -2
  122. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  123. package/registry/calendar/files/src/components/ui/calendar.tsx +9 -5
  124. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  125. package/registry/card/files/src/components/ui/card.tsx +8 -6
  126. package/registry/card/files/src/lib/kit.tsx +8 -5
  127. package/registry/chat.json +18 -0
  128. package/registry/checkbox/files/src/components/ui/checkbox.tsx +6 -5
  129. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  130. package/registry/chip-group/files/src/components/ui/chip-group.tsx +4 -2
  131. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  132. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  133. package/registry/coupon/files/src/components/ui/coupon.tsx +23 -20
  134. package/registry/coupon/files/src/components/ui/flip-card.tsx +1 -1
  135. package/registry/coupon/files/src/components/ui/tilt-card.tsx +2 -2
  136. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  137. package/registry/date-picker/files/src/components/ui/calendar.tsx +9 -5
  138. package/registry/date-picker/files/src/components/ui/date-picker.tsx +2 -2
  139. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  140. package/registry/dialog/files/src/components/ui/dialog.tsx +12 -6
  141. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  142. package/registry/donut-shop.json +18 -0
  143. package/registry/drawer/files/src/components/ui/drawer.tsx +25 -19
  144. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  145. package/registry/fintech-app.json +57 -0
  146. package/registry/flip-card/files/src/components/ui/flip-card.tsx +1 -1
  147. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  148. package/registry/floating-input/files/src/components/ui/floating-input.tsx +16 -6
  149. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  150. package/registry/food-app.json +55 -0
  151. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  152. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  153. package/registry/grain.json +17 -0
  154. package/registry/kit/files/src/lib/kit.tsx +8 -5
  155. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +1 -1
  156. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  157. package/registry/list-item/files/src/components/ui/list-item.tsx +20 -16
  158. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  159. package/registry/morphing-button-group.json +18 -0
  160. package/registry/orb-reveal.json +20 -0
  161. package/registry/otp-input/files/src/components/ui/otp-input.tsx +8 -6
  162. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  163. package/registry/password-input/files/src/components/ui/floating-input.tsx +16 -6
  164. package/registry/password-input/files/src/components/ui/password-input.tsx +2 -2
  165. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  166. package/registry/payment-card/files/src/components/ui/flip-card.tsx +1 -1
  167. package/registry/payment-card/files/src/components/ui/payment-card.tsx +27 -14
  168. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +2 -2
  169. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  170. package/registry/progress/files/src/components/ui/progress.tsx +8 -5
  171. package/registry/progress/files/src/lib/kit.tsx +8 -5
  172. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +15 -11
  173. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  174. package/registry/qr-code-button.json +18 -0
  175. package/registry/radio-group/files/src/components/ui/radio-group.tsx +10 -9
  176. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  177. package/registry/search-bar/files/src/components/ui/search-bar.tsx +13 -11
  178. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  179. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +4 -2
  180. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  181. package/registry/select/files/src/components/ui/select.tsx +3 -3
  182. package/registry/select/files/src/lib/kit.tsx +8 -5
  183. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  184. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  185. package/registry/slider/files/src/components/ui/slider.tsx +5 -4
  186. package/registry/slider/files/src/lib/kit.tsx +8 -5
  187. package/registry/spinner/files/src/components/ui/spinner.tsx +452 -34
  188. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  189. package/registry/spinner.json +1 -1
  190. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  191. package/registry/{pagination-dots → stateful-button}/files/src/lib/kit.tsx +8 -5
  192. package/registry/stateful-button.json +18 -0
  193. package/registry/stepper/files/src/components/ui/stepper.tsx +8 -4
  194. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  195. package/registry/switch/files/src/components/ui/switch.tsx +4 -3
  196. package/registry/switch/files/src/lib/kit.tsx +8 -5
  197. package/registry/tabs/files/src/components/ui/tabs.tsx +10 -8
  198. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  199. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  200. package/registry/text-animation/files/src/lib/kit.tsx +222 -0
  201. package/registry/text-animation.json +15 -0
  202. package/registry/textarea/files/src/components/ui/textarea.tsx +11 -9
  203. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  204. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  205. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +2 -2
  206. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  207. package/registry/time-picker/files/src/components/ui/time-picker.tsx +2 -2
  208. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  209. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  210. package/registry/timeline/files/src/components/ui/timeline.tsx +12 -10
  211. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  212. package/registry/toast/files/src/components/ui/button.tsx +4 -3
  213. package/registry/toast/files/src/components/ui/toast.tsx +13 -11
  214. package/registry/toast/files/src/lib/kit.tsx +8 -5
  215. package/schema/components.schema.json +1 -1
  216. package/registry/_docs/color-swatch.json +0 -66
  217. package/registry/_docs/kpi-card.json +0 -11
  218. package/registry/_docs/like-button.json +0 -72
  219. package/registry/_docs/morph-button.json +0 -108
  220. package/registry/_docs/pagination-dots.json +0 -88
  221. package/registry/_docs/shimmer-button.json +0 -78
  222. package/registry/_docs/tag-input.json +0 -128
  223. package/registry/color-swatch.json +0 -17
  224. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  225. package/registry/kpi-card.json +0 -14
  226. package/registry/like-button.json +0 -18
  227. package/registry/morph-button.json +0 -18
  228. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  229. package/registry/pagination-dots.json +0 -15
  230. package/registry/shimmer-button.json +0 -17
  231. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  232. package/registry/tag-input.json +0 -17
@@ -13,6 +13,7 @@ import Svg, { Circle, ClipPath, Defs, G, Line, LinearGradient, Path, Rect, Stop
13
13
 
14
14
  import { FlipCard, useFlipProgress } from '@/components/ui/flip-card';
15
15
  import { TiltCard } from '@/components/ui/tilt-card';
16
+ import { useKit } from '@/lib/kit';
16
17
 
17
18
  export type PaymentCardBrand = 'visa' | 'mastercard' | 'amex' | 'rupay' | 'none';
18
19
 
@@ -30,8 +31,8 @@ export interface PaymentCardProps {
30
31
  bank?: string;
31
32
  /** Small label top-right, e.g. "Debit", "World Elite". */
32
33
  label?: string;
33
- /** Card art — a photo or texture (`require('./card.jpg')` or `{ uri }`). Default: brushed graphite. */
34
- image?: ImageSourcePropType;
34
+ /** Card art — a photo or texture (`require('./card.jpg')` or `{ uri }`). Or any element drawn as the art, e.g. `<GrainGradient palette=\"ocean\" style={{ flex: 1 }} />`. Default: brushed graphite. */
35
+ image?: ImageSourcePropType | React.ReactElement;
35
36
  /** `light` text for dark art (default), `dark` text for pale art. */
36
37
  tone?: 'light' | 'dark';
37
38
  /** EMV chip finish. */
@@ -222,6 +223,8 @@ export function PaymentCard({
222
223
  labels,
223
224
  style,
224
225
  }: PaymentCardProps) {
226
+ const { font } = useKit();
227
+ const ff = font.family ? { fontFamily: font.family } : null;
225
228
  const controlled = flippedProp !== undefined;
226
229
  const [own, setOwn] = useState(defaultFlipped);
227
230
  // A frozen card always shows its front (where the frost and the label are).
@@ -253,7 +256,17 @@ export function PaymentCard({
253
256
 
254
257
  const art = (darken: number) => (
255
258
  <>
256
- {image ? <Image alt="" source={image} style={StyleSheet.absoluteFill} resizeMode="cover" accessibilityIgnoresInvertColors /> : <Graphite />}
259
+ {image ? (
260
+ React.isValidElement(image) ? (
261
+ <View style={StyleSheet.absoluteFill} pointerEvents="none">
262
+ {image}
263
+ </View>
264
+ ) : (
265
+ <Image alt="" source={image} style={StyleSheet.absoluteFill} resizeMode="cover" accessibilityIgnoresInvertColors />
266
+ )
267
+ ) : (
268
+ <Graphite />
269
+ )}
257
270
  {darken > 0 ? <View style={[StyleSheet.absoluteFill, { backgroundColor: `rgba(0,0,0,${darken})` }]} /> : null}
258
271
  </>
259
272
  );
@@ -265,28 +278,28 @@ export function PaymentCard({
265
278
  {tone === 'light' ? <Shade strength={image ? 0.38 : 0.2} /> : null}
266
279
  <View style={styles.inner}>
267
280
  <View style={styles.row}>
268
- <Text style={[{ color: ink, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 }, emboss]}>{bank ?? ''}</Text>
269
- {label ? <Text style={[{ color: soft, fontSize: 11, fontWeight: '600', letterSpacing: 1 }, emboss]}>{label.toUpperCase()}</Text> : null}
281
+ <Text style={[{ color: ink, fontSize: 16, fontWeight: '700', letterSpacing: -0.2 }, ff, emboss]}>{bank ?? ''}</Text>
282
+ {label ? <Text style={[{ color: soft, fontSize: 11, fontWeight: '600', letterSpacing: 1 }, ff, emboss]}>{label.toUpperCase()}</Text> : null}
270
283
  </View>
271
284
  <View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
272
285
  <Chip finish={chip} />
273
286
  <Contactless ink={ink} />
274
287
  </View>
275
288
  <View style={{ gap: 10 }}>
276
- <Text style={[{ color: ink, fontSize: 19, fontWeight: '500', letterSpacing: 2, fontVariant: ['tabular-nums'] }, emboss]}>
289
+ <Text style={[{ color: ink, fontSize: 19, fontWeight: '500', letterSpacing: 2, fontVariant: ['tabular-nums'] }, ff, emboss]}>
277
290
  {formatNumber(number, brand, masked)}
278
291
  </Text>
279
292
  <View style={[styles.row, { alignItems: 'flex-end' }]}>
280
293
  <View style={{ flexDirection: 'row', gap: 20, flexShrink: 1 }}>
281
294
  <View style={{ flexShrink: 1 }}>
282
- <Text style={[styles.tiny, { color: soft }]}>{text.cardholder}</Text>
283
- <Text style={[{ color: ink, fontSize: 13, fontWeight: '600', letterSpacing: 0.8 }, emboss]} numberOfLines={1}>
295
+ <Text style={[styles.tiny, ff, { color: soft }]}>{text.cardholder}</Text>
296
+ <Text style={[{ color: ink, fontSize: 13, fontWeight: '600', letterSpacing: 0.8 }, ff, emboss]} numberOfLines={1}>
284
297
  {name.toUpperCase()}
285
298
  </Text>
286
299
  </View>
287
300
  <View>
288
- <Text style={[styles.tiny, { color: soft }]}>{text.validThru}</Text>
289
- <Text style={[{ color: ink, fontSize: 13, fontWeight: '600', fontVariant: ['tabular-nums'] }, emboss]}>{expiry}</Text>
301
+ <Text style={[styles.tiny, ff, { color: soft }]}>{text.validThru}</Text>
302
+ <Text style={[{ color: ink, fontSize: 13, fontWeight: '600', fontVariant: ['tabular-nums'] }, ff, emboss]}>{expiry}</Text>
290
303
  </View>
291
304
  </View>
292
305
  <BrandMark brand={brand} ink={ink} />
@@ -296,7 +309,7 @@ export function PaymentCard({
296
309
  {frozen ? (
297
310
  <View style={[StyleSheet.absoluteFill, styles.center, { backgroundColor: 'rgba(214,232,255,0.28)' }]}>
298
311
  <View style={styles.frozenPill}>
299
- <Text style={{ color: '#ffffff', fontSize: 13, fontWeight: '700', letterSpacing: 0.4 }}>❄︎ {text.frozen}</Text>
312
+ <Text style={[{ color: '#ffffff', fontSize: 13, fontWeight: '700', letterSpacing: 0.4 }, ff]}>❄︎ {text.frozen}</Text>
300
313
  </View>
301
314
  </View>
302
315
  ) : null}
@@ -317,15 +330,15 @@ export function PaymentCard({
317
330
  </Svg>
318
331
  </View>
319
332
  <View style={styles.cvv}>
320
- <Text style={{ color: '#111', fontSize: 15, fontWeight: '700', fontStyle: 'italic', letterSpacing: 1.5, fontVariant: ['tabular-nums'] }}>{cvv ?? '•••'}</Text>
333
+ <Text style={[{ color: '#111', fontSize: 15, fontWeight: '700', fontStyle: 'italic', letterSpacing: 1.5, fontVariant: ['tabular-nums'] }, ff]}>{cvv ?? '•••'}</Text>
321
334
  </View>
322
335
  </View>
323
336
  <View style={styles.backFoot}>
324
337
  <View style={{ flex: 1, gap: 3 }}>
325
- <Text style={{ color: 'rgba(255,255,255,0.78)', fontSize: 9, lineHeight: 12 }}>
338
+ <Text style={[{ color: 'rgba(255,255,255,0.78)', fontSize: 9, lineHeight: 12 }, ff]}>
326
339
  {fill(text.legal)}
327
340
  </Text>
328
- <Text style={{ color: 'rgba(255,255,255,0.9)', fontSize: 10, fontWeight: '600' }}>{fill(text.back)}</Text>
341
+ <Text style={[{ color: 'rgba(255,255,255,0.9)', fontSize: 10, fontWeight: '600' }, ff]}>{fill(text.back)}</Text>
329
342
  </View>
330
343
  {/* Security hologram. */}
331
344
  <Svg width={40} height={30} viewBox="0 0 40 30">
@@ -1,4 +1,4 @@
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
3
  Animated,
4
4
  Easing,
@@ -267,7 +267,7 @@ export function TiltCard({
267
267
  const touching = useRef(false);
268
268
  const origin = useRef({ x: 0, y: 0 });
269
269
  const moved = useRef(false);
270
- const viewRef = useRef<View>(null);
270
+ const viewRef = useRef<ComponentRef<typeof View>>(null);
271
271
  const disabledRef = useRef(disabled);
272
272
  disabledRef.current = disabled;
273
273
 
@@ -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,7 +104,8 @@ export function Progress({
104
104
  striped = false,
105
105
  style,
106
106
  }: ProgressProps) {
107
- const { colors, dark } = useKit();
107
+ const { colors, dark, font } = useKit();
108
+ const ff = font.family ? { fontFamily: font.family } : null;
108
109
  const reduce = useReduceMotion();
109
110
  const top = max ?? (variant === 'steps' ? steps : 100);
110
111
  const unknown = indeterminate || value === undefined;
@@ -137,7 +138,7 @@ export function Progress({
137
138
  done={done}
138
139
  reduce={reduce}
139
140
  />
140
- {label ? <Text style={[styles.ringLabel, { color: colors.mutedForeground }]}>{label}</Text> : null}
141
+ {label ? <Text style={[styles.ringLabel, ff, { color: colors.mutedForeground }]}>{label}</Text> : null}
141
142
  </View>
142
143
  );
143
144
  }
@@ -147,13 +148,13 @@ export function Progress({
147
148
  const header =
148
149
  label || valueShown ? (
149
150
  <View style={styles.header}>
150
- <Text numberOfLines={1} style={[styles.label, { color: colors.foreground }]}>
151
+ <Text numberOfLines={1} style={[styles.label, ff, { color: colors.foreground }]}>
151
152
  {label}
152
153
  </Text>
153
154
  {checkShown ? (
154
155
  <DrawnCheck color={tint} size={18} />
155
156
  ) : valueShown ? (
156
- <Text style={[styles.value, { color: colors.mutedForeground }]}>{text}</Text>
157
+ <Text style={[styles.value, ff, { color: colors.mutedForeground }]}>{text}</Text>
157
158
  ) : null}
158
159
  </View>
159
160
  ) : null;
@@ -685,6 +686,8 @@ function Ring({
685
686
  reduce: boolean;
686
687
  }) {
687
688
  const { d, w, font } = RING[size];
689
+ const kit = useKit();
690
+ const ff = kit.font.family ? { fontFamily: kit.font.family } : null;
688
691
  const id = useId().replace(/[^a-zA-Z0-9]/g, '');
689
692
  const r = (d - w) / 2;
690
693
  const c = 2 * Math.PI * r;
@@ -835,7 +838,7 @@ function Ring({
835
838
  ],
836
839
  }}
837
840
  >
838
- <Text style={[styles.ringValue, { color: fg, fontSize: font }]}>
841
+ <Text style={[styles.ringValue, ff, { color: fg, fontSize: font }]}>
839
842
  {format ? format() : <RingPercent fill={fill} initial={ratio} />}
840
843
  {format ? null : <Text style={{ fontSize: font * 0.6, color: alpha(fg, 0.55) }}>%</Text>}
841
844
  </Text>
@@ -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
  };
@@ -70,7 +70,8 @@ export function ProgressSteps({
70
70
  accent,
71
71
  style,
72
72
  }: ProgressStepsProps) {
73
- const { colors, dark } = useKit();
73
+ const { colors, dark, font } = useKit();
74
+ const ff = font.family ? { fontFamily: font.family } : null;
74
75
  const reduce = useReduceMotion();
75
76
  const tint = accent ?? colors.accent;
76
77
  const items = steps.map((s) => (typeof s === 'string' ? { label: s } : s));
@@ -118,7 +119,7 @@ export function ProgressSteps({
118
119
  Animated.timing(pulse, { toValue: 1, duration: 1500, easing: Easing.out(Easing.quad), useNativeDriver: true }),
119
120
  Animated.delay(250),
120
121
  ]),
121
- { resetBeforeIteration: true }
122
+ { iterations: -1, resetBeforeIteration: true }
122
123
  );
123
124
  loop.start();
124
125
  return () => loop.stop();
@@ -146,6 +147,7 @@ export function ProgressSteps({
146
147
  numberOfLines={1}
147
148
  style={[
148
149
  styles.barLabel,
150
+ ff,
149
151
  { color: failed ? colors.danger : colors.foreground },
150
152
  {
151
153
  opacity: labelIn,
@@ -155,7 +157,7 @@ export function ProgressSteps({
155
157
  >
156
158
  {done ? 'All done' : items[shown]!.label}
157
159
  </Animated.Text>
158
- <Text style={[styles.barStep, { color: colors.mutedForeground }]}>
160
+ <Text style={[styles.barStep, ff, { color: colors.mutedForeground }]}>
159
161
  {done ? `${n} of ${n}` : `Step ${shown + 1} of ${n}`}
160
162
  </Text>
161
163
  </View>
@@ -255,7 +257,8 @@ function Step({
255
257
  reduce: boolean;
256
258
  onPress?: () => void;
257
259
  }) {
258
- const { colors } = useKit();
260
+ const { colors, font } = useKit();
261
+ const ff = font.family ? { fontFamily: font.family } : null;
259
262
  const press = useRef(new Animated.Value(0)).current;
260
263
  const pressTo = (v: number) =>
261
264
  Animated.spring(press, { toValue: v, useNativeDriver: true, ...(v ? motion.snappy : motion.bouncy) }).start();
@@ -301,9 +304,9 @@ function Step({
301
304
  {!last ? line(styles.vLine) : null}
302
305
  </View>
303
306
  <View style={[styles.vBody, !last && styles.vBodyGap]}>
304
- <Text style={[styles.vLabel, { color: labelColor }]}>{step.label}</Text>
307
+ <Text style={[styles.vLabel, ff, { color: labelColor }]}>{step.label}</Text>
305
308
  {step.description ? (
306
- <Text style={[styles.vDesc, { color: state === 'error' ? colors.danger : colors.mutedForeground }]}>
309
+ <Text style={[styles.vDesc, ff, { color: state === 'error' ? colors.danger : colors.mutedForeground }]}>
307
310
  {step.description}
308
311
  </Text>
309
312
  ) : null}
@@ -315,11 +318,11 @@ function Step({
315
318
  return (
316
319
  <Wrapper {...a11y} {...pressProps} style={styles.hCol}>
317
320
  <View style={styles.hTop}>{node}</View>
318
- <Text numberOfLines={2} style={[styles.hLabel, { color: labelColor }]}>
321
+ <Text numberOfLines={2} style={[styles.hLabel, ff, { color: labelColor }]}>
319
322
  {step.label}
320
323
  </Text>
321
324
  {step.description ? (
322
- <Text numberOfLines={2} style={[styles.hDesc, { color: colors.mutedForeground }]}>
325
+ <Text numberOfLines={2} style={[styles.hDesc, ff, { color: colors.mutedForeground }]}>
323
326
  {step.description}
324
327
  </Text>
325
328
  ) : null}
@@ -344,7 +347,8 @@ function Node({
344
347
  color: string;
345
348
  reduce: boolean;
346
349
  }) {
347
- const { colors, radius, dark } = useKit();
350
+ const { colors, radius, dark, font } = useKit();
351
+ const ff = font.family ? { fontFamily: font.family } : null;
348
352
  const clamp = 'clamp' as const;
349
353
  const round = { borderRadius: radius.capsule(NODE) };
350
354
  const activeIn = pNative.interpolate({ inputRange: [i - 0.3, i], outputRange: [0, 1], extrapolate: clamp });
@@ -370,7 +374,7 @@ function Node({
370
374
  <View
371
375
  style={[styles.node, round, { borderWidth: 1.5, borderColor: alpha(colors.foreground, dark ? 0.22 : 0.16) }]}
372
376
  >
373
- <Text style={[styles.num, { color: colors.mutedForeground }]}>{i + 1}</Text>
377
+ <Text style={[styles.num, ff, { color: colors.mutedForeground }]}>{i + 1}</Text>
374
378
  </View>
375
379
 
376
380
  {/* Current: soft halo, a ring that breathes outwards, accent ring with a tinted core. */}
@@ -411,7 +415,7 @@ function Node({
411
415
  ]}
412
416
  >
413
417
  <View style={[StyleSheet.absoluteFill, round, { backgroundColor: alpha(color, 0.12) }]} />
414
- <Text style={[styles.num, { color }]}>{i + 1}</Text>
418
+ <Text style={[styles.num, ff, { color }]}>{i + 1}</Text>
415
419
  </Animated.View>
416
420
 
417
421
  {/* 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
+ }
@@ -113,7 +113,8 @@ interface RadioItemProps {
113
113
  }
114
114
 
115
115
  function RadioItem({ option, active, onPress, variant, horizontal, first, tint, disabled }: RadioItemProps) {
116
- const { colors, radius } = useKit();
116
+ const { colors, radius, font } = useKit();
117
+ const ff = font.family ? { fontFamily: font.family } : null;
117
118
  // p: colours + svg (JS driver). dot: scale/opacity pops (native). press: press feedback (native).
118
119
  // ripple: a ring that expands and fades when the item gets selected, 0 → 1 (native, rests at 1).
119
120
  const p = useRef(new Animated.Value(active ? 1 : 0)).current;
@@ -154,17 +155,17 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
154
155
 
155
156
  const text = (
156
157
  <View style={styles.text}>
157
- <Text numberOfLines={2} style={[styles.label, { color: fg }]}>
158
+ <Text numberOfLines={2} style={[styles.label, ff, { color: fg }]}>
158
159
  {option.label}
159
160
  </Text>
160
- {option.description ? <Text style={[styles.description, { color: muted }]}>{option.description}</Text> : null}
161
+ {option.description ? <Text style={[styles.description, ff, { color: muted }]}>{option.description}</Text> : null}
161
162
  </View>
162
163
  );
163
164
 
164
165
  if (variant === 'card') {
165
166
  const lit = (from: string, to: string) => p.interpolate({ inputRange: [0, 1], outputRange: [from, to], extrapolate: 'clamp' });
166
167
  const hint = option.hint ? (
167
- <Animated.Text style={[styles.hint, horizontal && styles.hintColumn, { color: lit(fg, tint) }]}>{option.hint}</Animated.Text>
168
+ <Animated.Text style={[styles.hint, ff, horizontal && styles.hintColumn, { color: lit(fg, tint) }]}>{option.hint}</Animated.Text>
168
169
  ) : null;
169
170
  return (
170
171
  <Animated.View style={[horizontal && styles.flex, { opacity: disabled ? 0.5 : 1, transform: [{ scale: press }] }]}>
@@ -191,10 +192,10 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
191
192
  </Animated.View>
192
193
  ) : null}
193
194
  <View style={[styles.text, horizontal && styles.cardTextColumn]}>
194
- <Text numberOfLines={2} style={[styles.label, { color: fg }]}>
195
+ <Text numberOfLines={2} style={[styles.label, ff, { color: fg }]}>
195
196
  {option.label}
196
197
  </Text>
197
- {option.description ? <Text style={[styles.description, { color: muted }]}>{option.description}</Text> : null}
198
+ {option.description ? <Text style={[styles.description, ff, { color: muted }]}>{option.description}</Text> : null}
198
199
  {horizontal ? hint : null}
199
200
  </View>
200
201
  {horizontal ? null : hint}
@@ -241,11 +242,11 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
241
242
  <View style={styles.text}>
242
243
  <Animated.Text
243
244
  numberOfLines={1}
244
- style={[styles.label, { color: p.interpolate({ inputRange: [0, 1], outputRange: [fg, tint], extrapolate: 'clamp' }) }]}
245
+ style={[styles.label, ff, { color: p.interpolate({ inputRange: [0, 1], outputRange: [fg, tint], extrapolate: 'clamp' }) }]}
245
246
  >
246
247
  {option.label}
247
248
  </Animated.Text>
248
- {option.description ? <Text style={[styles.description, { color: muted }]}>{option.description}</Text> : null}
249
+ {option.description ? <Text style={[styles.description, ff, { color: muted }]}>{option.description}</Text> : null}
249
250
  </View>
250
251
  <Svg width={20} height={20} viewBox="0 0 24 24">
251
252
  <AnimatedPath
@@ -286,7 +287,7 @@ function RadioItem({ option, active, onPress, variant, horizontal, first, tint,
286
287
  </Animated.View>
287
288
  </Animated.View>
288
289
  {horizontal && !option.description ? (
289
- <Text style={[styles.label, { color: fg }]}>{option.label}</Text>
290
+ <Text style={[styles.label, ff, { color: fg }]}>{option.label}</Text>
290
291
  ) : (
291
292
  text
292
293
  )}
@@ -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,4 +1,4 @@
1
- import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
1
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
3
  Animated,
4
4
  Easing,
@@ -75,7 +75,7 @@ const webNoOutline = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWid
75
75
  * panel drops down while focused. The 'minimal' variant rests as a round icon and expands
76
76
  * into a full field when tapped.
77
77
  */
78
- export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBar(
78
+ export const SearchBar = forwardRef<ComponentRef<typeof TextInput>, SearchBarProps>(function SearchBar(
79
79
  {
80
80
  variant = 'default',
81
81
  size = 'md',
@@ -104,14 +104,15 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
104
104
  },
105
105
  ref
106
106
  ) {
107
- const { colors, radius } = useKit();
107
+ const { colors, radius, font } = useKit();
108
+ const ff = font.family ? { fontFamily: font.family } : null;
108
109
  const s = SIZES[size];
109
110
  const tint = accent ?? colors.accent;
110
111
  const minimal = variant === 'minimal';
111
112
  const cancelEnabled = !minimal && (showCancel ?? true);
112
113
 
113
- const inputRef = useRef<TextInput>(null);
114
- useImperativeHandle(ref, () => inputRef.current as TextInput);
114
+ const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
115
+ useImperativeHandle(ref, () => inputRef.current as ComponentRef<typeof TextInput>);
115
116
 
116
117
  const [focused, setFocused] = useState(false);
117
118
  const [inner, setInner] = useState(defaultValue ?? '');
@@ -293,7 +294,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
293
294
  if (minimal && !text) setExpanded(false);
294
295
  onBlur?.(e);
295
296
  }}
296
- style={[styles.input, { color: colors.foreground, fontSize: s.font, height: s.h - 3 }, webNoOutline]}
297
+ style={[styles.input, ff, { color: colors.foreground, fontSize: s.font, height: s.h - 3 }, webNoOutline]}
297
298
  />
298
299
  </Animated.View>
299
300
  <Animated.View
@@ -343,7 +344,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
343
344
  <Text
344
345
  pointerEvents="none"
345
346
  onLayout={(e) => setCancelW(Math.ceil(e.nativeEvent.layout.width) + 14)}
346
- style={[styles.cancelText, styles.measure, { fontSize: s.font }]}
347
+ style={[styles.cancelText, styles.measure, ff, { fontSize: s.font }]}
347
348
  >
348
349
  {cancelLabel}
349
350
  </Text>
@@ -366,7 +367,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(function SearchBa
366
367
  accessibilityLabel={cancelLabel}
367
368
  onPress={cancel}
368
369
  >
369
- <Text numberOfLines={1} style={[styles.cancelText, { color: tint, fontSize: s.font }]}>
370
+ <Text numberOfLines={1} style={[styles.cancelText, ff, { color: tint, fontSize: s.font }]}>
370
371
  {cancelLabel}
371
372
  </Text>
372
373
  </Pressable>
@@ -397,7 +398,8 @@ function SuggestionPanel({
397
398
  tint: string;
398
399
  onSelect: (item: SearchSuggestion) => void;
399
400
  }) {
400
- const { colors, radius } = useKit();
401
+ const { colors, radius, font } = useKit();
402
+ const ff = font.family ? { fontFamily: font.family } : null;
401
403
  const [mounted, setMounted] = useState(open);
402
404
  const enter = useRef(new Animated.Value(0)).current;
403
405
  const last = useRef(items);
@@ -443,11 +445,11 @@ function SuggestionPanel({
443
445
  <View style={[styles.optionIcon, { backgroundColor: colors.muted }]}>
444
446
  <Icon name={item.icon ?? 'search'} size={15} color={item.icon === 'trending' ? tint : colors.mutedForeground} />
445
447
  </View>
446
- <Text numberOfLines={1} style={[styles.optionText, { color: colors.mutedForeground }]}>
448
+ <Text numberOfLines={1} style={[styles.optionText, ff, { color: colors.mutedForeground }]}>
447
449
  <Highlight label={item.label} query={query} color={colors.foreground} />
448
450
  </Text>
449
451
  {item.hint ? (
450
- <Text numberOfLines={1} style={[styles.hint, { color: colors.mutedForeground }]}>
452
+ <Text numberOfLines={1} style={[styles.hint, ff, { color: colors.mutedForeground }]}>
451
453
  {item.hint}
452
454
  </Text>
453
455
  ) : null}
@@ -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
  };
@@ -57,7 +57,7 @@ const SIZES: Record<SegmentedControlSize, { h: number; font: number; px: number;
57
57
  const INSET = 3;
58
58
  // Liquid move: the edge in the direction of travel leads, the other one trails behind.
59
59
  const LEAD = motion.snappy;
60
- const TRAIL = { damping: 20, stiffness: 150, mass: 0.9 };
60
+ const TRAIL = { damping: 24, stiffness: 150, mass: 0.9 };
61
61
 
62
62
  /**
63
63
  * Segmented control with a liquid indicator: its leading edge springs ahead and the trailing
@@ -274,6 +274,8 @@ interface SegmentProps {
274
274
  }
275
275
 
276
276
  function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size, height, activeFg, idleFg, disabled }: SegmentProps) {
277
+ const { font } = useKit();
278
+ const ff = font.family ? { fontFamily: font.family } : null;
277
279
  // a: text colour (JS). press: content squish, pop: little bounce on selection (native, same view).
278
280
  const a = useRef(new Animated.Value(active ? 1 : 0)).current;
279
281
  const press = useRef(new Animated.Value(1)).current;
@@ -315,7 +317,7 @@ function Segment({ option, active, onPress, onPressIn, onLayout, fullWidth, size
315
317
  <Animated.View style={[styles.content, { transform: [{ scale: Animated.multiply(press, pop) }] }]}>
316
318
  {icon ? <View style={{ minWidth: size.icon, alignItems: 'center' }}>{icon}</View> : null}
317
319
  {option.label ? (
318
- <Animated.Text numberOfLines={1} style={[styles.label, { fontSize: size.font, color }]}>
320
+ <Animated.Text numberOfLines={1} style={[styles.label, ff, { fontSize: size.font, color }]}>
319
321
  {option.label}
320
322
  </Animated.Text>
321
323
  ) : null}
@@ -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,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
3
  Animated,
4
4
  Dimensions,
@@ -219,8 +219,8 @@ export function Select<T extends string | number = string>({
219
219
  const [anchor, setAnchor] = useState({ x: 0, y: 0, w: 0, h: 0 });
220
220
  const [hi, setHi] = useState(-1); // highlighted row (keyboard / hover, web)
221
221
  const [kb, setKb] = useState(0); // on-screen keyboard height while the dropdown is open
222
- const fieldRef = useRef<View>(null);
223
- const scrollRef = useRef<ScrollView>(null);
222
+ const fieldRef = useRef<ComponentRef<typeof View>>(null);
223
+ const scrollRef = useRef<ComponentRef<typeof ScrollView>>(null);
224
224
  const rowBox = useRef<Record<string, { y: number; h: number }>>({});
225
225
  const scrollY = useRef(0);
226
226
  const viewH = useRef(0);