@expokit/cli 0.4.4 → 0.5.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 (239) hide show
  1. package/README.md +30 -3
  2. package/bin/expo-kit.js +749 -288
  3. package/bin/lib.mjs +310 -3
  4. package/package.json +1 -1
  5. package/registry/_docs/action-sheet.json +1 -1
  6. package/registry/_docs/alert.json +6 -0
  7. package/registry/_docs/bottom-sheet.json +6 -0
  8. package/registry/_docs/bullet.json +6 -0
  9. package/registry/_docs/dialog.json +1 -1
  10. package/registry/_docs/drawer.json +3 -3
  11. package/registry/_docs/glass-search-bar.json +1 -1
  12. package/registry/_docs/grain.json +6 -1
  13. package/registry/_docs/isometric-bar.json +6 -0
  14. package/registry/_docs/liquidglass-tab.json +1 -1
  15. package/registry/_docs/password-input.json +1 -1
  16. package/registry/_docs/pixel-bar.json +6 -0
  17. package/registry/_docs/polar-area.json +6 -0
  18. package/registry/_docs/qr-code-button.json +9 -3
  19. package/registry/_docs/scratch-card.json +7 -1
  20. package/registry/_docs/search-bar.json +1 -1
  21. package/registry/_docs/select.json +2 -2
  22. package/registry/_docs/slide-to-confirm.json +2 -2
  23. package/registry/_docs/spinner.json +1 -1
  24. package/registry/_docs/stateful-button.json +2 -2
  25. package/registry/_docs/swipe-deck.json +1 -1
  26. package/registry/_docs/time-picker.json +2 -2
  27. package/registry/_docs/timeline-chart.json +6 -0
  28. package/registry/_docs/toast.json +15 -3
  29. package/registry/_docs/treemap.json +6 -0
  30. package/registry/_docs/waterfall.json +6 -0
  31. package/registry/accordion/files/src/components/ui/accordion.tsx +6 -4
  32. package/registry/accordion/files/src/lib/kit.tsx +363 -11
  33. package/registry/accordion.json +1 -0
  34. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +43 -48
  35. package/registry/action-sheet/files/src/lib/kit.tsx +363 -11
  36. package/registry/action-sheet.json +1 -0
  37. package/registry/alert/files/src/components/ui/alert.tsx +19 -12
  38. package/registry/alert/files/src/lib/kit.tsx +363 -11
  39. package/registry/alert.json +3 -4
  40. package/registry/assistant-app.json +1 -0
  41. package/registry/avatar/files/src/components/ui/avatar.tsx +84 -66
  42. package/registry/avatar/files/src/lib/kit.tsx +363 -11
  43. package/registry/avatar.json +1 -0
  44. package/registry/badge/files/src/components/ui/badge.tsx +34 -34
  45. package/registry/badge/files/src/lib/kit.tsx +363 -11
  46. package/registry/badge.json +1 -0
  47. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +28 -27
  48. package/registry/bottom-nav/files/src/lib/kit.tsx +363 -11
  49. package/registry/bottom-nav.json +5 -0
  50. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +353 -289
  51. package/registry/bottom-sheet/files/src/lib/kit.tsx +363 -11
  52. package/registry/bottom-sheet.json +5 -1
  53. package/registry/bullet.json +1 -0
  54. package/registry/button/files/src/components/ui/button.tsx +18 -16
  55. package/registry/button/files/src/lib/kit.tsx +363 -11
  56. package/registry/button-3d/files/src/components/ui/button-3d.tsx +2 -2
  57. package/registry/button-3d/files/src/lib/kit.tsx +363 -11
  58. package/registry/button-3d.json +1 -0
  59. package/registry/button.json +1 -0
  60. package/registry/calendar/files/src/components/ui/calendar.tsx +74 -76
  61. package/registry/calendar/files/src/lib/kit.tsx +363 -11
  62. package/registry/calendar.json +1 -0
  63. package/registry/card/files/src/components/ui/card.tsx +3 -3
  64. package/registry/card/files/src/lib/kit.tsx +363 -11
  65. package/registry/card-stack.json +1 -0
  66. package/registry/card.json +1 -0
  67. package/registry/carousel.json +1 -0
  68. package/registry/chat.json +7 -0
  69. package/registry/checkbox/files/src/components/ui/checkbox.tsx +4 -4
  70. package/registry/checkbox/files/src/lib/kit.tsx +363 -11
  71. package/registry/checkbox.json +1 -0
  72. package/registry/checkout.json +1 -0
  73. package/registry/chip-group/files/src/components/ui/chip-group.tsx +22 -22
  74. package/registry/chip-group/files/src/lib/kit.tsx +363 -11
  75. package/registry/chip-group.json +1 -0
  76. package/registry/confetti/files/src/components/ui/confetti.tsx +15 -14
  77. package/registry/confetti.json +3 -6
  78. package/registry/coupon/files/src/components/ui/coupon.tsx +42 -42
  79. package/registry/coupon/files/src/components/ui/flip-card.tsx +6 -5
  80. package/registry/coupon/files/src/components/ui/tilt-card.tsx +76 -91
  81. package/registry/coupon/files/src/lib/kit.tsx +363 -11
  82. package/registry/coupon/files/src/lib/ticket-shape.tsx +54 -44
  83. package/registry/coupon.json +1 -0
  84. package/registry/cube-pager.json +1 -0
  85. package/registry/dashboard.json +1 -0
  86. package/registry/date-picker/files/src/components/ui/calendar.tsx +74 -76
  87. package/registry/date-picker/files/src/components/ui/date-picker.tsx +55 -56
  88. package/registry/date-picker/files/src/lib/kit.tsx +363 -11
  89. package/registry/date-picker.json +1 -0
  90. package/registry/dialog/files/src/components/ui/dialog.tsx +60 -56
  91. package/registry/dialog/files/src/lib/kit.tsx +363 -11
  92. package/registry/dialog.json +1 -0
  93. package/registry/donut-shop.json +1 -0
  94. package/registry/drawer/files/src/components/ui/drawer.tsx +73 -69
  95. package/registry/drawer/files/src/lib/kit.tsx +363 -11
  96. package/registry/drawer.json +1 -0
  97. package/registry/fab.json +1 -0
  98. package/registry/fintech-app.json +1 -0
  99. package/registry/flip-calendar/files/src/components/ui/flip-calendar.tsx +92 -86
  100. package/registry/flip-calendar/files/src/lib/kit.tsx +363 -11
  101. package/registry/flip-calendar.json +7 -0
  102. package/registry/flip-card/files/src/components/ui/flip-card.tsx +6 -5
  103. package/registry/flip-card/files/src/lib/kit.tsx +363 -11
  104. package/registry/flip-card.json +1 -0
  105. package/registry/floating-input/files/src/components/ui/floating-input.tsx +33 -33
  106. package/registry/floating-input/files/src/lib/kit.tsx +363 -11
  107. package/registry/floating-input.json +1 -0
  108. package/registry/food-app.json +1 -0
  109. package/registry/gift-card.json +1 -0
  110. package/registry/glass-button.json +5 -0
  111. package/registry/glass-nav-bar.json +1 -0
  112. package/registry/glass-search-bar.json +1 -0
  113. package/registry/glass-segmented.json +1 -0
  114. package/registry/glass-sheet.json +1 -0
  115. package/registry/glass-tab-bar.json +5 -0
  116. package/registry/glass.json +1 -0
  117. package/registry/grain/files/src/components/ui/grain.tsx +16 -14
  118. package/registry/grain.json +8 -6
  119. package/registry/inbox.json +1 -0
  120. package/registry/isometric-bar.json +1 -0
  121. package/registry/kit/files/src/lib/kit.tsx +363 -11
  122. package/registry/kit.json +1 -0
  123. package/registry/liquidglass-tab/files/src/app/_layout.tsx +0 -1
  124. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +23 -9
  125. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +2 -2
  126. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +1 -1
  127. package/registry/liquidglass-tab.json +1 -0
  128. package/registry/list-item/files/src/components/ui/list-item.tsx +42 -42
  129. package/registry/list-item/files/src/lib/kit.tsx +363 -11
  130. package/registry/list-item.json +1 -0
  131. package/registry/loyalty-card.json +1 -0
  132. package/registry/morphing-button-group.json +1 -0
  133. package/registry/number-ticker.json +1 -0
  134. package/registry/offers.json +1 -0
  135. package/registry/onboarding.json +1 -0
  136. package/registry/orb-reveal.json +1 -0
  137. package/registry/otp-input/files/src/components/ui/otp-input.tsx +48 -29
  138. package/registry/otp-input/files/src/lib/kit.tsx +363 -11
  139. package/registry/otp-input.json +1 -0
  140. package/registry/parallax-layers.json +1 -0
  141. package/registry/parallax-scroll.json +1 -0
  142. package/registry/password-input/files/src/components/ui/floating-input.tsx +33 -33
  143. package/registry/password-input/files/src/components/ui/password-input.tsx +29 -25
  144. package/registry/password-input/files/src/lib/kit.tsx +363 -11
  145. package/registry/password-input.json +1 -0
  146. package/registry/payment-card/files/src/components/ui/flip-card.tsx +6 -5
  147. package/registry/payment-card/files/src/components/ui/payment-card.tsx +17 -17
  148. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +76 -91
  149. package/registry/payment-card/files/src/lib/kit.tsx +363 -11
  150. package/registry/payment-card.json +1 -0
  151. package/registry/paywall.json +1 -0
  152. package/registry/pixel-bar.json +1 -0
  153. package/registry/polar-area.json +1 -0
  154. package/registry/profile.json +1 -0
  155. package/registry/progress/files/src/components/ui/progress.tsx +29 -29
  156. package/registry/progress/files/src/lib/kit.tsx +363 -11
  157. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +20 -20
  158. package/registry/progress-steps/files/src/lib/kit.tsx +363 -11
  159. package/registry/progress-steps.json +1 -0
  160. package/registry/progress.json +1 -0
  161. package/registry/qr-code-button.json +1 -0
  162. package/registry/radio-group/files/src/components/ui/radio-group.tsx +19 -19
  163. package/registry/radio-group/files/src/lib/kit.tsx +363 -11
  164. package/registry/radio-group.json +1 -0
  165. package/registry/rating.json +1 -0
  166. package/registry/ring-carousel.json +1 -0
  167. package/registry/ruler-time-picker/files/src/components/ui/ruler-time-picker.tsx +98 -82
  168. package/registry/ruler-time-picker/files/src/lib/kit.tsx +363 -11
  169. package/registry/ruler-time-picker.json +7 -0
  170. package/registry/scratch-card.json +1 -0
  171. package/registry/search-bar/files/src/components/ui/search-bar.tsx +25 -21
  172. package/registry/search-bar/files/src/lib/kit.tsx +363 -11
  173. package/registry/search-bar.json +1 -0
  174. package/registry/search-results.json +4 -0
  175. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +8 -7
  176. package/registry/segmented-control/files/src/lib/kit.tsx +363 -11
  177. package/registry/segmented-control.json +1 -0
  178. package/registry/select/files/src/components/ui/select.tsx +93 -88
  179. package/registry/select/files/src/lib/kit.tsx +363 -11
  180. package/registry/select.json +6 -0
  181. package/registry/settings.json +1 -0
  182. package/registry/sign-in.json +1 -0
  183. package/registry/skeleton/files/src/components/ui/skeleton.tsx +149 -80
  184. package/registry/skeleton/files/src/lib/kit.tsx +363 -11
  185. package/registry/skeleton.json +1 -0
  186. package/registry/slide-to-confirm.json +1 -0
  187. package/registry/slider/files/src/components/ui/slider.tsx +103 -96
  188. package/registry/slider/files/src/lib/kit.tsx +363 -11
  189. package/registry/slider.json +5 -0
  190. package/registry/spin-wheel.json +1 -0
  191. package/registry/spinner/files/src/components/ui/spinner.tsx +39 -29
  192. package/registry/spinner/files/src/lib/kit.tsx +363 -11
  193. package/registry/spinner.json +6 -0
  194. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +39 -32
  195. package/registry/stateful-button/files/src/lib/kit.tsx +363 -11
  196. package/registry/stateful-button.json +5 -0
  197. package/registry/stepper/files/src/components/ui/stepper.tsx +38 -36
  198. package/registry/stepper/files/src/lib/kit.tsx +363 -11
  199. package/registry/stepper.json +1 -0
  200. package/registry/swipe-deck.json +1 -0
  201. package/registry/swipeable-row.json +4 -0
  202. package/registry/switch/files/src/components/ui/switch.tsx +39 -42
  203. package/registry/switch/files/src/lib/kit.tsx +363 -11
  204. package/registry/switch.json +1 -0
  205. package/registry/tabs/files/src/components/ui/tabs.tsx +59 -62
  206. package/registry/tabs/files/src/lib/kit.tsx +363 -11
  207. package/registry/tabs.json +1 -0
  208. package/registry/text-animation/files/src/components/ui/text-animation.tsx +55 -42
  209. package/registry/text-animation/files/src/lib/kit.tsx +363 -11
  210. package/registry/text-animation.json +7 -0
  211. package/registry/textarea/files/src/components/ui/textarea.tsx +10 -10
  212. package/registry/textarea/files/src/lib/kit.tsx +363 -11
  213. package/registry/textarea.json +1 -0
  214. package/registry/ticket-card.json +1 -0
  215. package/registry/ticket-shape/files/src/lib/kit.tsx +363 -11
  216. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +54 -44
  217. package/registry/ticket-shape.json +1 -0
  218. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +76 -91
  219. package/registry/tilt-card/files/src/lib/kit.tsx +363 -11
  220. package/registry/tilt-card.json +1 -0
  221. package/registry/time-picker/files/src/components/ui/time-picker.tsx +122 -119
  222. package/registry/time-picker/files/src/lib/kit.tsx +363 -11
  223. package/registry/time-picker.json +1 -0
  224. package/registry/timeline/files/src/components/ui/avatar.tsx +84 -66
  225. package/registry/timeline/files/src/components/ui/timeline.tsx +44 -41
  226. package/registry/timeline/files/src/lib/kit.tsx +363 -11
  227. package/registry/timeline-chart.json +1 -0
  228. package/registry/timeline.json +1 -0
  229. package/registry/toast/files/src/components/ui/toast.tsx +541 -463
  230. package/registry/toast/files/src/lib/kit.tsx +363 -11
  231. package/registry/toast.json +7 -5
  232. package/registry/treemap.json +1 -0
  233. package/registry/waterfall.json +1 -0
  234. package/registry/wheel-list.json +1 -0
  235. package/schema/components.schema.json +1 -0
  236. package/registry/alert/files/src/components/ui/button.tsx +0 -290
  237. package/registry/confetti/files/src/lib/kit.tsx +0 -320
  238. package/registry/grain/files/src/lib/kit.tsx +0 -320
  239. package/registry/toast/files/src/components/ui/button.tsx +0 -290
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
1
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
3
  AccessibilityInfo,
4
4
  Animated,
@@ -7,7 +7,6 @@ import {
7
7
  Keyboard,
8
8
  KeyboardAvoidingView,
9
9
  Modal,
10
- PanResponder,
11
10
  Platform,
12
11
  Pressable,
13
12
  ScrollView,
@@ -21,7 +20,7 @@ import {
21
20
  } from 'react-native';
22
21
  import Svg, { Circle, Path } from 'react-native-svg';
23
22
 
24
- import { alpha, motion, readableOn, useKit, useLayout } from '@/lib/kit';
23
+ import { alpha, motion, readableOn, useKit, useLayout, usePanResponder } from '@/lib/kit';
25
24
 
26
25
  export type SelectPresentation = 'dropdown' | 'sheet';
27
26
  /** @deprecated Use `SelectPresentation`. */
@@ -165,12 +164,12 @@ export function Select<T extends string | number = string>({
165
164
  maxChips = 2,
166
165
  label,
167
166
  floatingLabel = true,
168
- placeholder = 'Select…',
167
+ placeholder: placeholderProp,
169
168
  presentation,
170
169
  variant,
171
170
  size = 'md',
172
171
  searchable,
173
- searchPlaceholder = 'Search',
172
+ searchPlaceholder: searchPlaceholderProp,
174
173
  emptyText,
175
174
  title,
176
175
  error,
@@ -192,7 +191,9 @@ export function Select<T extends string | number = string>({
192
191
  optionStyle,
193
192
  optionTextStyle,
194
193
  }: SelectProps<T>) {
195
- const { colors, radius, dark, font, shadow } = useKit();
194
+ const { colors, radius, dark, font, shadow, labels } = useKit();
195
+ const placeholder = placeholderProp ?? labels.selectPlaceholder;
196
+ const searchPlaceholder = searchPlaceholderProp ?? labels.search;
196
197
  const reduce = useReduceMotion();
197
198
  const ff = font.family ? { fontFamily: font.family } : null;
198
199
  const tint = accent ?? colors.accent;
@@ -232,17 +233,14 @@ export function Select<T extends string | number = string>({
232
233
  const viewH = useRef(0);
233
234
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
234
235
  const alive = useRef(true);
235
- // Read by the sheet's pan responder, which is created once.
236
- const reduceRef = useRef(reduce);
237
- reduceRef.current = reduce;
238
236
 
239
237
  const floating = open || picked.length > 0;
240
- const float = useRef(new Animated.Value(floating ? 1 : 0)).current; // native
241
- const focus = useRef(new Animated.Value(0)).current; // JS: colours
242
- const halo = useRef(new Animated.Value(0)).current; // native: focus halo
243
- const show = useRef(new Animated.Value(0)).current; // native: backdrop + sheet/dropdown
244
- const drag = useRef(new Animated.Value(0)).current; // native: sheet drag offset
245
- const shake = useRef(new Animated.Value(0)).current; // native
238
+ const [float] = useState(() => new Animated.Value(floating ? 1 : 0)); // native
239
+ const [focus] = useState(() => new Animated.Value(0)); // JS: colours
240
+ const [halo] = useState(() => new Animated.Value(0)); // native: focus halo
241
+ const [show] = useState(() => new Animated.Value(0)); // native: backdrop + sheet/dropdown
242
+ const [drag] = useState(() => new Animated.Value(0)); // native: sheet drag offset
243
+ const [shake] = useState(() => new Animated.Value(0)); // native
246
244
 
247
245
  useEffect(() => {
248
246
  alive.current = true;
@@ -419,7 +417,8 @@ export function Select<T extends string | number = string>({
419
417
  let i = from < 0 ? (dir > 0 ? -1 : n) : from;
420
418
  for (let k = 0; k < n; k++) {
421
419
  i = (i + dir + n) % n;
422
- if (!flat[i].disabled) {
420
+ const o = flat[i];
421
+ if (o && !o.disabled) {
423
422
  setHi(i);
424
423
  reveal(i);
425
424
  return;
@@ -429,35 +428,37 @@ export function Select<T extends string | number = string>({
429
428
 
430
429
  // Web keyboard: ↑/↓ move, Home/End jump, Enter picks, Escape/Tab close.
431
430
  const onKey = useRef<(e: KeyboardEvent) => void>(() => {});
432
- onKey.current = (e) => {
433
- switch (e.key) {
434
- case 'ArrowDown':
435
- case 'ArrowUp':
436
- e.preventDefault();
437
- step(e.key === 'ArrowDown' ? 1 : -1);
438
- break;
439
- case 'Home':
440
- case 'End':
441
- if (searchable && query) return; // let the caret move in the search field
442
- e.preventDefault();
443
- step(e.key === 'Home' ? 1 : -1, e.key === 'Home' ? -1 : flat.length);
444
- break;
445
- case 'Enter':
446
- if (hi >= 0 && flat[hi]) {
431
+ useLayoutEffect(() => {
432
+ onKey.current = (e) => {
433
+ switch (e.key) {
434
+ case 'ArrowDown':
435
+ case 'ArrowUp':
447
436
  e.preventDefault();
448
- choose(flat[hi]);
449
- }
450
- break;
451
- case 'Escape':
452
- e.preventDefault();
453
- e.stopPropagation();
454
- close();
455
- break;
456
- case 'Tab':
457
- close();
458
- break;
459
- }
460
- };
437
+ step(e.key === 'ArrowDown' ? 1 : -1);
438
+ break;
439
+ case 'Home':
440
+ case 'End':
441
+ if (searchable && query) return; // let the caret move in the search field
442
+ e.preventDefault();
443
+ step(e.key === 'Home' ? 1 : -1, e.key === 'Home' ? -1 : flat.length);
444
+ break;
445
+ case 'Enter':
446
+ if (hi >= 0 && flat[hi]) {
447
+ e.preventDefault();
448
+ choose(flat[hi]);
449
+ }
450
+ break;
451
+ case 'Escape':
452
+ e.preventDefault();
453
+ e.stopPropagation();
454
+ close();
455
+ break;
456
+ case 'Tab':
457
+ close();
458
+ break;
459
+ }
460
+ };
461
+ });
461
462
  useEffect(() => {
462
463
  if (!open || !isWeb || typeof document === 'undefined') return;
463
464
  const handler = (e: KeyboardEvent) => onKey.current(e);
@@ -466,21 +467,19 @@ export function Select<T extends string | number = string>({
466
467
  }, [open]);
467
468
 
468
469
  // Drag the sheet's grabber down to dismiss.
469
- const pan = useMemo(() => {
470
- const back = () =>
471
- reduceRef.current
472
- ? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
473
- : Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
474
- return PanResponder.create({
475
- onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
476
- onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
477
- onPanResponderRelease: (_, g) => {
478
- if (g.dy > 110 || g.vy > 1.1) setOpen(false);
479
- else back();
480
- },
481
- onPanResponderTerminate: back,
482
- });
483
- }, [drag]);
470
+ const back = () =>
471
+ reduce
472
+ ? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
473
+ : Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
474
+ const pan = usePanResponder({
475
+ onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
476
+ onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
477
+ onPanResponderRelease: (_, g) => {
478
+ if (g.dy > 110 || g.vy > 1.1) setOpen(false);
479
+ else back();
480
+ },
481
+ onPanResponderTerminate: back,
482
+ });
484
483
 
485
484
  const active = error ? colors.danger : tint;
486
485
  const borderColor = focus.interpolate({
@@ -524,7 +523,7 @@ export function Select<T extends string | number = string>({
524
523
  onPress={() => onSearch('')}
525
524
  hitSlop={12}
526
525
  accessibilityRole="button"
527
- accessibilityLabel="Clear search"
526
+ accessibilityLabel={labels.clearSearch}
528
527
  style={[styles.clear, { backgroundColor: alpha(colors.mutedForeground, 0.22) }]}
529
528
  >
530
529
  <Icon name="x" size={11} color={colors.foreground} strokeWidth={2.4} />
@@ -588,11 +587,11 @@ export function Select<T extends string | number = string>({
588
587
  <Icon name="search" size={dense ? 16 : 20} color={colors.mutedForeground} />
589
588
  </View>
590
589
  <Text style={[styles.emptyTitle, ff, { color: colors.foreground, fontSize: dense ? 14 : 15 }]}>
591
- {q ? 'No results' : 'No options'}
590
+ {q ? labels.noResults : labels.noOptions}
592
591
  </Text>
593
592
  {q ? (
594
593
  <Text style={[styles.emptyText, ff, { color: colors.mutedForeground }]}>
595
- {emptyText ?? `Nothing matches “${query.trim()}”.`}
594
+ {emptyText ?? labels.nothingMatches(query.trim())}
596
595
  </Text>
597
596
  ) : null}
598
597
  </View>
@@ -611,7 +610,7 @@ export function Select<T extends string | number = string>({
611
610
  ]}
612
611
  >
613
612
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footDenseText, ff, { color: colors.mutedForeground }]}>
614
- Clear
613
+ {labels.clear}
615
614
  </Text>
616
615
  </Pressable>
617
616
  <View style={styles.footMid}>
@@ -623,7 +622,7 @@ export function Select<T extends string | number = string>({
623
622
  style={({ pressed }) => [styles.footSolid, { backgroundColor: tint, borderRadius: radius.capsule(32), opacity: pressed ? 0.85 : 1 }]}
624
623
  >
625
624
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footDenseText, styles.footStrong, ff, { color: readableOn(tint) }]}>
626
- Done
625
+ {labels.done}
627
626
  </Text>
628
627
  </Pressable>
629
628
  </View>
@@ -636,7 +635,7 @@ export function Select<T extends string | number = string>({
636
635
  style={({ pressed }) => [styles.footBtn, { borderRadius: radius.md, opacity: many.length ? (pressed ? 0.6 : 1) : 0.4 }]}
637
636
  >
638
637
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>
639
- Clear
638
+ {labels.clear}
640
639
  </Text>
641
640
  </Pressable>
642
641
  <Pressable
@@ -645,7 +644,7 @@ export function Select<T extends string | number = string>({
645
644
  style={({ pressed }) => [styles.footBtn, styles.footMain, { backgroundColor: tint, borderRadius: radius.md, opacity: pressed ? 0.85 : 1 }]}
646
645
  >
647
646
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, styles.footStrong, styles.tabular, ff, { color: readableOn(tint) }]}>
648
- {many.length ? `Done · ${many.length}` : 'Done'}
647
+ {many.length ? labels.doneCount(many.length) : labels.done}
649
648
  </Text>
650
649
  </Pressable>
651
650
  </View>
@@ -702,7 +701,7 @@ export function Select<T extends string | number = string>({
702
701
  disabled={disabled}
703
702
  accessibilityRole="combobox"
704
703
  accessibilityLabel={label}
705
- accessibilityValue={{ text: picked.length ? picked.map((o) => o.label).join(', ') : 'None selected' }}
704
+ accessibilityValue={{ text: picked.length ? picked.map((o) => o.label).join(', ') : labels.noneSelected }}
706
705
  accessibilityState={{ expanded: open, disabled: !!disabled }}
707
706
  style={{ opacity: disabled ? 0.5 : 1 }}
708
707
  >
@@ -759,7 +758,7 @@ export function Select<T extends string | number = string>({
759
758
  onPress={() => item.option && remove(item.option.value)}
760
759
  hitSlop={12}
761
760
  accessibilityRole="button"
762
- accessibilityLabel={`Remove ${item.option.label}`}
761
+ accessibilityLabel={labels.remove(item.option.label)}
763
762
  style={({ pressed }) => [
764
763
  styles.chipX,
765
764
  {
@@ -777,7 +776,7 @@ export function Select<T extends string | number = string>({
777
776
  ) : (
778
777
  <View
779
778
  accessible
780
- accessibilityLabel={maxChips > 0 ? `${item.more} more` : `${item.more} selected`}
779
+ accessibilityLabel={maxChips > 0 ? labels.moreCount(item.more) : labels.selectedCount(item.more)}
781
780
  style={[styles.chip, styles.more, { minHeight: sz.chip, backgroundColor: chipBg, borderRadius: radius.capsule(sz.chip) }]}
782
781
  >
783
782
  {maxChips > 0 ? (
@@ -788,7 +787,7 @@ export function Select<T extends string | number = string>({
788
787
  <RollingNumber value={item.more} distance={sz.chipFont} reduce={reduce} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont }]} />
789
788
  {maxChips > 0 ? null : (
790
789
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont, marginLeft: 3 }]}>
791
- selected
790
+ {labels.selectedSuffix}
792
791
  </Text>
793
792
  )}
794
793
  </View>
@@ -852,7 +851,7 @@ export function Select<T extends string | number = string>({
852
851
 
853
852
  <Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
854
853
  <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: asSheet ? colors.overlay : 'transparent', opacity: show }]}>
855
- <Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityLabel="Close" accessibilityRole="button" />
854
+ <Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityLabel={labels.close} accessibilityRole="button" />
856
855
  </Animated.View>
857
856
 
858
857
  {asSheet ? (
@@ -891,7 +890,7 @@ export function Select<T extends string | number = string>({
891
890
  },
892
891
  ]}
893
892
  >
894
- <View {...pan.panHandlers} style={styles.sheetHeader}>
893
+ <View {...pan} style={styles.sheetHeader}>
895
894
  {!wide ? <View style={[styles.grabber, { backgroundColor: colors.border }]} /> : null}
896
895
  <Text style={[styles.title, ff, { color: colors.foreground }]}>{title ?? label}</Text>
897
896
  {multiple ? (
@@ -899,7 +898,7 @@ export function Select<T extends string | number = string>({
899
898
  {many.length ? (
900
899
  <CountBadge count={many.length} tint={tint} reduce={reduce} />
901
900
  ) : (
902
- <Text style={[styles.subtitle, ff, { color: colors.mutedForeground }]}>Choose one or more</Text>
901
+ <Text style={[styles.subtitle, ff, { color: colors.mutedForeground }]}>{labels.chooseOneOrMore}</Text>
903
902
  )}
904
903
  </View>
905
904
  ) : null}
@@ -942,7 +941,7 @@ export function Select<T extends string | number = string>({
942
941
  }
943
942
 
944
943
  /** Groups options by `group` (first-appearance order); a single untitled section when none are grouped. */
945
- function sectionsOf<T extends string | number>(list: SelectOption<T>[]): { title?: string; rows: SelectOption<T>[] }[] {
944
+ function sectionsOf<T extends string | number>(list: SelectOption<T>[]): { title?: string | undefined; rows: SelectOption<T>[] }[] {
946
945
  if (!list.some((o) => o.group)) return [{ rows: list }];
947
946
  const map = new Map<string, SelectOption<T>[]>();
948
947
  list.forEach((o) => map.set(o.group ?? '', [...(map.get(o.group ?? '') ?? []), o]));
@@ -982,7 +981,7 @@ function OptionRow<T extends string | number>({
982
981
  }) {
983
982
  const { colors, radius, font, dark } = useKit();
984
983
  const ff = font.family ? { fontFamily: font.family } : null;
985
- const check = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: scale + opacity
984
+ const [check] = useState(() => new Animated.Value(selected ? 1 : 0)); // native: scale + opacity
986
985
  useEffect(() => {
987
986
  // Reduce Motion: the check only fades (no pop / twist).
988
987
  if (reduce) Animated.timing(check, { toValue: selected ? 1 : 0, duration: 150, useNativeDriver: true }).start();
@@ -1082,7 +1081,10 @@ function usePresence<V>(items: { key: string; item: V }[]) {
1082
1081
  const out: Present<V>[] = [];
1083
1082
  let next = 0;
1084
1083
  const emitUpTo = (end: number) => {
1085
- for (; next <= end; next++) out.push({ ...items[next], leaving: false, appear: ready.current });
1084
+ for (; next <= end; next++) {
1085
+ const e = items[next];
1086
+ if (e) out.push({ ...e, leaving: false, appear: ready.current });
1087
+ }
1086
1088
  };
1087
1089
  for (const e of prev.current) {
1088
1090
  if (live.has(e.key)) emitUpTo(items.findIndex((x) => x.key === e.key));
@@ -1121,11 +1123,13 @@ function AnimatedChip({
1121
1123
  onGone: () => void;
1122
1124
  children: React.ReactNode;
1123
1125
  }) {
1124
- const pop = useRef(new Animated.Value(appear ? 0 : 1)).current; // native: scale + opacity
1125
- const size = useRef(new Animated.Value(1)).current; // JS: width + margin of the outer slot
1126
+ const [pop] = useState(() => new Animated.Value(appear ? 0 : 1)); // native: scale + opacity
1127
+ const [size] = useState(() => new Animated.Value(1)); // JS: width + margin of the outer slot
1126
1128
  const [w, setW] = useState(0);
1127
1129
  const gone = useRef(onGone);
1128
- gone.current = onGone;
1130
+ useLayoutEffect(() => {
1131
+ gone.current = onGone;
1132
+ });
1129
1133
 
1130
1134
  useEffect(() => {
1131
1135
  let alive = true;
@@ -1238,12 +1242,13 @@ function RollingNumber({ value, style, distance = 10, reduce }: { value: number;
1238
1242
 
1239
1243
  /** "N selected" pill whose number rolls; it pops out when the count drops to 0. */
1240
1244
  function CountBadge({ count, tint, reduce }: { count: number; tint: string; reduce: boolean }) {
1241
- const { radius, font } = useKit();
1245
+ const { radius, font, labels } = useKit();
1242
1246
  const ff = font.family ? { fontFamily: font.family } : null;
1243
1247
  const on = count > 0;
1244
- const vis = useRef(new Animated.Value(on ? 1 : 0)).current; // native
1245
- const last = useRef(count);
1246
- if (on) last.current = count;
1248
+ const [vis] = useState(() => new Animated.Value(on ? 1 : 0)); // native
1249
+ // The last non-zero count, kept on screen while the badge pops out at 0.
1250
+ const [last, setLast] = useState(count);
1251
+ if (on && count !== last) setLast(count);
1247
1252
  useEffect(() => {
1248
1253
  const a = reduce
1249
1254
  ? Animated.timing(vis, { toValue: on ? 1 : 0, duration: 150, useNativeDriver: true })
@@ -1255,7 +1260,7 @@ function CountBadge({ count, tint, reduce }: { count: number; tint: string; redu
1255
1260
  return (
1256
1261
  <Animated.View
1257
1262
  accessible
1258
- accessibilityLabel={`${last.current} selected`}
1263
+ accessibilityLabel={labels.selectedCount(last)}
1259
1264
  accessibilityLiveRegion="polite"
1260
1265
  style={[
1261
1266
  styles.badge,
@@ -1267,9 +1272,9 @@ function CountBadge({ count, tint, reduce }: { count: number; tint: string; redu
1267
1272
  },
1268
1273
  ]}
1269
1274
  >
1270
- <RollingNumber value={last.current} distance={9} reduce={reduce} style={text} />
1275
+ <RollingNumber value={last} distance={9} reduce={reduce} style={text} />
1271
1276
  <Text maxFontSizeMultiplier={FONT_CAP} style={[text, styles.badgeSuffix]}>
1272
- selected
1277
+ {labels.selectedSuffix}
1273
1278
  </Text>
1274
1279
  </Animated.View>
1275
1280
  );
@@ -1297,7 +1302,7 @@ function Highlight({ text, query, tint, style }: { text: string; query: string;
1297
1302
  /** Helper / error line that fades and slides in; errors get an icon and are announced. */
1298
1303
  function FieldMessage({ text, error, reduce, style }: { text: string; error: boolean; reduce: boolean; style?: StyleProp<TextStyle> }) {
1299
1304
  const { colors, font } = useKit();
1300
- const enter = useRef(new Animated.Value(0)).current; // native
1305
+ const [enter] = useState(() => new Animated.Value(0)); // native
1301
1306
  useEffect(() => {
1302
1307
  enter.setValue(0);
1303
1308
  if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
@@ -1319,7 +1324,7 @@ function FieldMessage({ text, error, reduce, style }: { text: string; error: boo
1319
1324
  }
1320
1325
 
1321
1326
  function Chevron({ open, reduce, color, size, right }: { open: boolean; reduce: boolean; color: string; size: number; right: number }) {
1322
- const turn = useRef(new Animated.Value(open ? 1 : 0)).current;
1327
+ const [turn] = useState(() => new Animated.Value(open ? 1 : 0));
1323
1328
  useEffect(() => {
1324
1329
  if (reduce) turn.setValue(open ? 1 : 0);
1325
1330
  else Animated.spring(turn, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();