@expokit/cli 0.4.3 → 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 (242) hide show
  1. package/README.md +30 -3
  2. package/bin/expo-kit.js +749 -288
  3. package/bin/lib.mjs +313 -4
  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/flip-calendar.json +106 -0
  12. package/registry/_docs/glass-search-bar.json +1 -1
  13. package/registry/_docs/grain.json +6 -1
  14. package/registry/_docs/isometric-bar.json +6 -0
  15. package/registry/_docs/liquidglass-tab.json +1 -1
  16. package/registry/_docs/password-input.json +1 -1
  17. package/registry/_docs/paywall.json +144 -0
  18. package/registry/_docs/pixel-bar.json +6 -0
  19. package/registry/_docs/polar-area.json +6 -0
  20. package/registry/_docs/qr-code-button.json +9 -3
  21. package/registry/_docs/ruler-time-picker.json +147 -0
  22. package/registry/_docs/scratch-card.json +7 -1
  23. package/registry/_docs/search-bar.json +1 -1
  24. package/registry/_docs/select.json +2 -2
  25. package/registry/_docs/slide-to-confirm.json +2 -2
  26. package/registry/_docs/spinner.json +1 -1
  27. package/registry/_docs/stateful-button.json +2 -2
  28. package/registry/_docs/swipe-deck.json +1 -1
  29. package/registry/_docs/time-picker.json +2 -2
  30. package/registry/_docs/timeline-chart.json +6 -0
  31. package/registry/_docs/toast.json +15 -3
  32. package/registry/_docs/treemap.json +6 -0
  33. package/registry/_docs/waterfall.json +6 -0
  34. package/registry/accordion/files/src/components/ui/accordion.tsx +6 -4
  35. package/registry/accordion/files/src/lib/kit.tsx +462 -12
  36. package/registry/accordion.json +1 -0
  37. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +50 -58
  38. package/registry/action-sheet/files/src/lib/kit.tsx +462 -12
  39. package/registry/action-sheet.json +1 -0
  40. package/registry/alert/files/src/components/ui/alert.tsx +20 -13
  41. package/registry/alert/files/src/lib/kit.tsx +462 -12
  42. package/registry/alert.json +3 -4
  43. package/registry/assistant-app.json +1 -0
  44. package/registry/avatar/files/src/components/ui/avatar.tsx +89 -74
  45. package/registry/avatar/files/src/lib/kit.tsx +462 -12
  46. package/registry/avatar.json +1 -0
  47. package/registry/badge/files/src/components/ui/badge.tsx +36 -36
  48. package/registry/badge/files/src/lib/kit.tsx +462 -12
  49. package/registry/badge.json +1 -0
  50. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +39 -44
  51. package/registry/bottom-nav/files/src/lib/kit.tsx +462 -12
  52. package/registry/bottom-nav.json +5 -0
  53. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +357 -300
  54. package/registry/bottom-sheet/files/src/lib/kit.tsx +462 -12
  55. package/registry/bottom-sheet.json +5 -1
  56. package/registry/bullet.json +1 -0
  57. package/registry/button/files/src/components/ui/button.tsx +23 -34
  58. package/registry/button/files/src/lib/kit.tsx +462 -12
  59. package/registry/button-3d/files/src/components/ui/button-3d.tsx +6 -9
  60. package/registry/button-3d/files/src/lib/kit.tsx +462 -12
  61. package/registry/button-3d.json +1 -0
  62. package/registry/button.json +1 -0
  63. package/registry/calendar/files/src/components/ui/calendar.tsx +77 -79
  64. package/registry/calendar/files/src/lib/kit.tsx +462 -12
  65. package/registry/calendar.json +1 -0
  66. package/registry/card/files/src/components/ui/card.tsx +7 -6
  67. package/registry/card/files/src/lib/kit.tsx +462 -12
  68. package/registry/card-stack.json +1 -0
  69. package/registry/card.json +1 -0
  70. package/registry/carousel.json +1 -0
  71. package/registry/chat.json +7 -0
  72. package/registry/checkbox/files/src/components/ui/checkbox.tsx +4 -4
  73. package/registry/checkbox/files/src/lib/kit.tsx +462 -12
  74. package/registry/checkbox.json +1 -0
  75. package/registry/checkout.json +1 -0
  76. package/registry/chip-group/files/src/components/ui/chip-group.tsx +24 -24
  77. package/registry/chip-group/files/src/lib/kit.tsx +462 -12
  78. package/registry/chip-group.json +1 -0
  79. package/registry/confetti/files/src/components/ui/confetti.tsx +15 -14
  80. package/registry/confetti.json +3 -6
  81. package/registry/coupon/files/src/components/ui/coupon.tsx +47 -52
  82. package/registry/coupon/files/src/components/ui/flip-card.tsx +9 -14
  83. package/registry/coupon/files/src/components/ui/tilt-card.tsx +80 -101
  84. package/registry/coupon/files/src/lib/kit.tsx +462 -12
  85. package/registry/coupon/files/src/lib/ticket-shape.tsx +54 -44
  86. package/registry/coupon.json +1 -0
  87. package/registry/cube-pager.json +1 -0
  88. package/registry/dashboard.json +1 -0
  89. package/registry/date-picker/files/src/components/ui/calendar.tsx +77 -79
  90. package/registry/date-picker/files/src/components/ui/date-picker.tsx +64 -71
  91. package/registry/date-picker/files/src/lib/kit.tsx +462 -12
  92. package/registry/date-picker.json +1 -0
  93. package/registry/dialog/files/src/components/ui/dialog.tsx +66 -68
  94. package/registry/dialog/files/src/lib/kit.tsx +462 -12
  95. package/registry/dialog.json +1 -0
  96. package/registry/donut-shop.json +1 -0
  97. package/registry/drawer/files/src/components/ui/drawer.tsx +76 -88
  98. package/registry/drawer/files/src/lib/kit.tsx +462 -12
  99. package/registry/drawer.json +1 -0
  100. package/registry/fab.json +1 -0
  101. package/registry/fintech-app.json +1 -0
  102. package/registry/flip-calendar/files/src/components/ui/flip-calendar.tsx +632 -0
  103. package/registry/flip-calendar/files/src/lib/kit.tsx +672 -0
  104. package/registry/flip-calendar.json +24 -0
  105. package/registry/flip-card/files/src/components/ui/flip-card.tsx +9 -14
  106. package/registry/flip-card/files/src/lib/kit.tsx +462 -12
  107. package/registry/flip-card.json +1 -0
  108. package/registry/floating-input/files/src/components/ui/floating-input.tsx +33 -33
  109. package/registry/floating-input/files/src/lib/kit.tsx +462 -12
  110. package/registry/floating-input.json +1 -0
  111. package/registry/food-app.json +1 -0
  112. package/registry/gift-card.json +1 -0
  113. package/registry/glass-button.json +5 -0
  114. package/registry/glass-nav-bar.json +1 -0
  115. package/registry/glass-search-bar.json +1 -0
  116. package/registry/glass-segmented.json +1 -0
  117. package/registry/glass-sheet.json +1 -0
  118. package/registry/glass-tab-bar.json +5 -0
  119. package/registry/glass.json +1 -0
  120. package/registry/grain/files/src/components/ui/grain.tsx +16 -14
  121. package/registry/grain.json +8 -6
  122. package/registry/inbox.json +1 -0
  123. package/registry/isometric-bar.json +1 -0
  124. package/registry/kit/files/src/lib/kit.tsx +462 -12
  125. package/registry/kit.json +1 -0
  126. package/registry/liquidglass-tab/files/src/app/_layout.tsx +0 -1
  127. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +29 -16
  128. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +33 -19
  129. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +1 -1
  130. package/registry/liquidglass-tab.json +1 -0
  131. package/registry/list-item/files/src/components/ui/list-item.tsx +43 -43
  132. package/registry/list-item/files/src/lib/kit.tsx +462 -12
  133. package/registry/list-item.json +1 -0
  134. package/registry/loyalty-card.json +1 -0
  135. package/registry/morphing-button-group.json +1 -0
  136. package/registry/number-ticker.json +1 -0
  137. package/registry/offers.json +1 -0
  138. package/registry/onboarding.json +1 -0
  139. package/registry/orb-reveal.json +1 -0
  140. package/registry/otp-input/files/src/components/ui/otp-input.tsx +49 -30
  141. package/registry/otp-input/files/src/lib/kit.tsx +462 -12
  142. package/registry/otp-input.json +1 -0
  143. package/registry/parallax-layers.json +1 -0
  144. package/registry/parallax-scroll.json +1 -0
  145. package/registry/password-input/files/src/components/ui/floating-input.tsx +33 -33
  146. package/registry/password-input/files/src/components/ui/password-input.tsx +29 -25
  147. package/registry/password-input/files/src/lib/kit.tsx +462 -12
  148. package/registry/password-input.json +1 -0
  149. package/registry/payment-card/files/src/components/ui/flip-card.tsx +9 -14
  150. package/registry/payment-card/files/src/components/ui/payment-card.tsx +26 -20
  151. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +80 -101
  152. package/registry/payment-card/files/src/lib/kit.tsx +462 -12
  153. package/registry/payment-card.json +1 -0
  154. package/registry/paywall.json +23 -0
  155. package/registry/pixel-bar.json +1 -0
  156. package/registry/polar-area.json +1 -0
  157. package/registry/profile.json +1 -0
  158. package/registry/progress/files/src/components/ui/progress.tsx +31 -29
  159. package/registry/progress/files/src/lib/kit.tsx +462 -12
  160. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +21 -21
  161. package/registry/progress-steps/files/src/lib/kit.tsx +462 -12
  162. package/registry/progress-steps.json +1 -0
  163. package/registry/progress.json +1 -0
  164. package/registry/qr-code-button.json +1 -0
  165. package/registry/radio-group/files/src/components/ui/radio-group.tsx +19 -19
  166. package/registry/radio-group/files/src/lib/kit.tsx +462 -12
  167. package/registry/radio-group.json +1 -0
  168. package/registry/rating.json +1 -0
  169. package/registry/ring-carousel.json +1 -0
  170. package/registry/ruler-time-picker/files/src/components/ui/ruler-time-picker.tsx +832 -0
  171. package/registry/ruler-time-picker/files/src/lib/kit.tsx +672 -0
  172. package/registry/ruler-time-picker.json +26 -0
  173. package/registry/scratch-card.json +1 -0
  174. package/registry/search-bar/files/src/components/ui/search-bar.tsx +28 -28
  175. package/registry/search-bar/files/src/lib/kit.tsx +462 -12
  176. package/registry/search-bar.json +1 -0
  177. package/registry/search-results.json +4 -0
  178. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +11 -17
  179. package/registry/segmented-control/files/src/lib/kit.tsx +462 -12
  180. package/registry/segmented-control.json +1 -0
  181. package/registry/select/files/src/components/ui/select.tsx +107 -104
  182. package/registry/select/files/src/lib/kit.tsx +462 -12
  183. package/registry/select.json +6 -0
  184. package/registry/settings.json +1 -0
  185. package/registry/sign-in.json +1 -0
  186. package/registry/skeleton/files/src/components/ui/skeleton.tsx +149 -80
  187. package/registry/skeleton/files/src/lib/kit.tsx +462 -12
  188. package/registry/skeleton.json +1 -0
  189. package/registry/slide-to-confirm.json +1 -0
  190. package/registry/slider/files/src/components/ui/slider.tsx +106 -108
  191. package/registry/slider/files/src/lib/kit.tsx +462 -12
  192. package/registry/slider.json +5 -0
  193. package/registry/spin-wheel.json +1 -0
  194. package/registry/spinner/files/src/components/ui/spinner.tsx +39 -29
  195. package/registry/spinner/files/src/lib/kit.tsx +462 -12
  196. package/registry/spinner.json +6 -0
  197. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +39 -32
  198. package/registry/stateful-button/files/src/lib/kit.tsx +462 -12
  199. package/registry/stateful-button.json +5 -0
  200. package/registry/stepper/files/src/components/ui/stepper.tsx +40 -38
  201. package/registry/stepper/files/src/lib/kit.tsx +462 -12
  202. package/registry/stepper.json +1 -0
  203. package/registry/swipe-deck.json +1 -0
  204. package/registry/swipeable-row.json +4 -0
  205. package/registry/switch/files/src/components/ui/switch.tsx +41 -47
  206. package/registry/switch/files/src/lib/kit.tsx +462 -12
  207. package/registry/switch.json +1 -0
  208. package/registry/tabs/files/src/components/ui/tabs.tsx +61 -64
  209. package/registry/tabs/files/src/lib/kit.tsx +462 -12
  210. package/registry/tabs.json +1 -0
  211. package/registry/text-animation/files/src/components/ui/text-animation.tsx +55 -42
  212. package/registry/text-animation/files/src/lib/kit.tsx +462 -12
  213. package/registry/text-animation.json +7 -0
  214. package/registry/textarea/files/src/components/ui/textarea.tsx +10 -10
  215. package/registry/textarea/files/src/lib/kit.tsx +462 -12
  216. package/registry/textarea.json +1 -0
  217. package/registry/ticket-card.json +1 -0
  218. package/registry/ticket-shape/files/src/lib/kit.tsx +462 -12
  219. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +54 -44
  220. package/registry/ticket-shape.json +1 -0
  221. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +80 -101
  222. package/registry/tilt-card/files/src/lib/kit.tsx +462 -12
  223. package/registry/tilt-card.json +1 -0
  224. package/registry/time-picker/files/src/components/ui/time-picker.tsx +136 -131
  225. package/registry/time-picker/files/src/lib/kit.tsx +462 -12
  226. package/registry/time-picker.json +1 -0
  227. package/registry/timeline/files/src/components/ui/avatar.tsx +89 -74
  228. package/registry/timeline/files/src/components/ui/timeline.tsx +49 -57
  229. package/registry/timeline/files/src/lib/kit.tsx +462 -12
  230. package/registry/timeline-chart.json +1 -0
  231. package/registry/timeline.json +1 -0
  232. package/registry/toast/files/src/components/ui/toast.tsx +543 -465
  233. package/registry/toast/files/src/lib/kit.tsx +462 -12
  234. package/registry/toast.json +7 -5
  235. package/registry/treemap.json +1 -0
  236. package/registry/waterfall.json +1 -0
  237. package/registry/wheel-list.json +1 -0
  238. package/schema/components.schema.json +1 -0
  239. package/registry/alert/files/src/components/ui/button.tsx +0 -303
  240. package/registry/confetti/files/src/lib/kit.tsx +0 -222
  241. package/registry/grain/files/src/lib/kit.tsx +0 -222
  242. package/registry/toast/files/src/components/ui/button.tsx +0 -303
@@ -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,
@@ -15,14 +14,13 @@ import {
15
14
  Text,
16
15
  TextInput,
17
16
  View,
18
- useWindowDimensions,
19
17
  type StyleProp,
20
18
  type TextStyle,
21
19
  type ViewStyle,
22
20
  } from 'react-native';
23
21
  import Svg, { Circle, Path } from 'react-native-svg';
24
22
 
25
- import { alpha, motion, readableOn, useKit } from '@/lib/kit';
23
+ import { alpha, motion, readableOn, useKit, useLayout, usePanResponder } from '@/lib/kit';
26
24
 
27
25
  export type SelectPresentation = 'dropdown' | 'sheet';
28
26
  /** @deprecated Use `SelectPresentation`. */
@@ -166,12 +164,12 @@ export function Select<T extends string | number = string>({
166
164
  maxChips = 2,
167
165
  label,
168
166
  floatingLabel = true,
169
- placeholder = 'Select…',
167
+ placeholder: placeholderProp,
170
168
  presentation,
171
169
  variant,
172
170
  size = 'md',
173
171
  searchable,
174
- searchPlaceholder = 'Search',
172
+ searchPlaceholder: searchPlaceholderProp,
175
173
  emptyText,
176
174
  title,
177
175
  error,
@@ -193,7 +191,9 @@ export function Select<T extends string | number = string>({
193
191
  optionStyle,
194
192
  optionTextStyle,
195
193
  }: SelectProps<T>) {
196
- const { colors, radius, dark, font } = useKit();
194
+ const { colors, radius, dark, font, shadow, labels } = useKit();
195
+ const placeholder = placeholderProp ?? labels.selectPlaceholder;
196
+ const searchPlaceholder = searchPlaceholderProp ?? labels.search;
197
197
  const reduce = useReduceMotion();
198
198
  const ff = font.family ? { fontFamily: font.family } : null;
199
199
  const tint = accent ?? colors.accent;
@@ -205,10 +205,12 @@ export function Select<T extends string | number = string>({
205
205
  const HEIGHT = typeof userField.height === 'number' ? userField.height : sz.h;
206
206
  const fieldRadius = typeof userField.borderRadius === 'number' ? userField.borderRadius : radius.scale(12 + sz.corner);
207
207
  const panelRadius = Math.max(fieldRadius, radius.scale(14));
208
- const { width: winW, height: winH } = useWindowDimensions();
208
+ const layout = useLayout();
209
+ const { width: winW, height: winH } = layout;
209
210
  const items = useMemo(() => options.map(normalize), [options]);
210
211
  const asSheet = (presentation ?? variant ?? 'dropdown') === 'sheet';
211
- const wide = winW >= 640;
212
+ // A centred card whenever the kit caps the sheet (foldables, tablets, landscape phones).
213
+ const wide = layout.sheetWidth < winW;
212
214
 
213
215
  const [inner, setInner] = useState<T | null>(defaultValue);
214
216
  const [innerMany, setInnerMany] = useState<T[]>(defaultValues ?? []);
@@ -231,17 +233,14 @@ export function Select<T extends string | number = string>({
231
233
  const viewH = useRef(0);
232
234
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
233
235
  const alive = useRef(true);
234
- // Read by the sheet's pan responder, which is created once.
235
- const reduceRef = useRef(reduce);
236
- reduceRef.current = reduce;
237
236
 
238
237
  const floating = open || picked.length > 0;
239
- const float = useRef(new Animated.Value(floating ? 1 : 0)).current; // native
240
- const focus = useRef(new Animated.Value(0)).current; // JS: colours
241
- const halo = useRef(new Animated.Value(0)).current; // native: focus halo
242
- const show = useRef(new Animated.Value(0)).current; // native: backdrop + sheet/dropdown
243
- const drag = useRef(new Animated.Value(0)).current; // native: sheet drag offset
244
- 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
245
244
 
246
245
  useEffect(() => {
247
246
  alive.current = true;
@@ -418,7 +417,8 @@ export function Select<T extends string | number = string>({
418
417
  let i = from < 0 ? (dir > 0 ? -1 : n) : from;
419
418
  for (let k = 0; k < n; k++) {
420
419
  i = (i + dir + n) % n;
421
- if (!flat[i].disabled) {
420
+ const o = flat[i];
421
+ if (o && !o.disabled) {
422
422
  setHi(i);
423
423
  reveal(i);
424
424
  return;
@@ -428,35 +428,37 @@ export function Select<T extends string | number = string>({
428
428
 
429
429
  // Web keyboard: ↑/↓ move, Home/End jump, Enter picks, Escape/Tab close.
430
430
  const onKey = useRef<(e: KeyboardEvent) => void>(() => {});
431
- onKey.current = (e) => {
432
- switch (e.key) {
433
- case 'ArrowDown':
434
- case 'ArrowUp':
435
- e.preventDefault();
436
- step(e.key === 'ArrowDown' ? 1 : -1);
437
- break;
438
- case 'Home':
439
- case 'End':
440
- if (searchable && query) return; // let the caret move in the search field
441
- e.preventDefault();
442
- step(e.key === 'Home' ? 1 : -1, e.key === 'Home' ? -1 : flat.length);
443
- break;
444
- case 'Enter':
445
- if (hi >= 0 && flat[hi]) {
431
+ useLayoutEffect(() => {
432
+ onKey.current = (e) => {
433
+ switch (e.key) {
434
+ case 'ArrowDown':
435
+ case 'ArrowUp':
446
436
  e.preventDefault();
447
- choose(flat[hi]);
448
- }
449
- break;
450
- case 'Escape':
451
- e.preventDefault();
452
- e.stopPropagation();
453
- close();
454
- break;
455
- case 'Tab':
456
- close();
457
- break;
458
- }
459
- };
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
+ });
460
462
  useEffect(() => {
461
463
  if (!open || !isWeb || typeof document === 'undefined') return;
462
464
  const handler = (e: KeyboardEvent) => onKey.current(e);
@@ -465,21 +467,19 @@ export function Select<T extends string | number = string>({
465
467
  }, [open]);
466
468
 
467
469
  // Drag the sheet's grabber down to dismiss.
468
- const pan = useMemo(() => {
469
- const back = () =>
470
- reduceRef.current
471
- ? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
472
- : Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
473
- return PanResponder.create({
474
- onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
475
- onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
476
- onPanResponderRelease: (_, g) => {
477
- if (g.dy > 110 || g.vy > 1.1) setOpen(false);
478
- else back();
479
- },
480
- onPanResponderTerminate: back,
481
- });
482
- }, [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
+ });
483
483
 
484
484
  const active = error ? colors.danger : tint;
485
485
  const borderColor = focus.interpolate({
@@ -523,7 +523,7 @@ export function Select<T extends string | number = string>({
523
523
  onPress={() => onSearch('')}
524
524
  hitSlop={12}
525
525
  accessibilityRole="button"
526
- accessibilityLabel="Clear search"
526
+ accessibilityLabel={labels.clearSearch}
527
527
  style={[styles.clear, { backgroundColor: alpha(colors.mutedForeground, 0.22) }]}
528
528
  >
529
529
  <Icon name="x" size={11} color={colors.foreground} strokeWidth={2.4} />
@@ -587,11 +587,11 @@ export function Select<T extends string | number = string>({
587
587
  <Icon name="search" size={dense ? 16 : 20} color={colors.mutedForeground} />
588
588
  </View>
589
589
  <Text style={[styles.emptyTitle, ff, { color: colors.foreground, fontSize: dense ? 14 : 15 }]}>
590
- {q ? 'No results' : 'No options'}
590
+ {q ? labels.noResults : labels.noOptions}
591
591
  </Text>
592
592
  {q ? (
593
593
  <Text style={[styles.emptyText, ff, { color: colors.mutedForeground }]}>
594
- {emptyText ?? `Nothing matches “${query.trim()}”.`}
594
+ {emptyText ?? labels.nothingMatches(query.trim())}
595
595
  </Text>
596
596
  ) : null}
597
597
  </View>
@@ -610,7 +610,7 @@ export function Select<T extends string | number = string>({
610
610
  ]}
611
611
  >
612
612
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footDenseText, ff, { color: colors.mutedForeground }]}>
613
- Clear
613
+ {labels.clear}
614
614
  </Text>
615
615
  </Pressable>
616
616
  <View style={styles.footMid}>
@@ -622,7 +622,7 @@ export function Select<T extends string | number = string>({
622
622
  style={({ pressed }) => [styles.footSolid, { backgroundColor: tint, borderRadius: radius.capsule(32), opacity: pressed ? 0.85 : 1 }]}
623
623
  >
624
624
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footDenseText, styles.footStrong, ff, { color: readableOn(tint) }]}>
625
- Done
625
+ {labels.done}
626
626
  </Text>
627
627
  </Pressable>
628
628
  </View>
@@ -635,7 +635,7 @@ export function Select<T extends string | number = string>({
635
635
  style={({ pressed }) => [styles.footBtn, { borderRadius: radius.md, opacity: many.length ? (pressed ? 0.6 : 1) : 0.4 }]}
636
636
  >
637
637
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>
638
- Clear
638
+ {labels.clear}
639
639
  </Text>
640
640
  </Pressable>
641
641
  <Pressable
@@ -644,7 +644,7 @@ export function Select<T extends string | number = string>({
644
644
  style={({ pressed }) => [styles.footBtn, styles.footMain, { backgroundColor: tint, borderRadius: radius.md, opacity: pressed ? 0.85 : 1 }]}
645
645
  >
646
646
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, styles.footStrong, styles.tabular, ff, { color: readableOn(tint) }]}>
647
- {many.length ? `Done · ${many.length}` : 'Done'}
647
+ {many.length ? labels.doneCount(many.length) : labels.done}
648
648
  </Text>
649
649
  </Pressable>
650
650
  </View>
@@ -701,7 +701,7 @@ export function Select<T extends string | number = string>({
701
701
  disabled={disabled}
702
702
  accessibilityRole="combobox"
703
703
  accessibilityLabel={label}
704
- 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 }}
705
705
  accessibilityState={{ expanded: open, disabled: !!disabled }}
706
706
  style={{ opacity: disabled ? 0.5 : 1 }}
707
707
  >
@@ -758,7 +758,7 @@ export function Select<T extends string | number = string>({
758
758
  onPress={() => item.option && remove(item.option.value)}
759
759
  hitSlop={12}
760
760
  accessibilityRole="button"
761
- accessibilityLabel={`Remove ${item.option.label}`}
761
+ accessibilityLabel={labels.remove(item.option.label)}
762
762
  style={({ pressed }) => [
763
763
  styles.chipX,
764
764
  {
@@ -776,7 +776,7 @@ export function Select<T extends string | number = string>({
776
776
  ) : (
777
777
  <View
778
778
  accessible
779
- accessibilityLabel={maxChips > 0 ? `${item.more} more` : `${item.more} selected`}
779
+ accessibilityLabel={maxChips > 0 ? labels.moreCount(item.more) : labels.selectedCount(item.more)}
780
780
  style={[styles.chip, styles.more, { minHeight: sz.chip, backgroundColor: chipBg, borderRadius: radius.capsule(sz.chip) }]}
781
781
  >
782
782
  {maxChips > 0 ? (
@@ -787,7 +787,7 @@ export function Select<T extends string | number = string>({
787
787
  <RollingNumber value={item.more} distance={sz.chipFont} reduce={reduce} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont }]} />
788
788
  {maxChips > 0 ? null : (
789
789
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.chipText, ff, { color: tint, fontSize: sz.chipFont, marginLeft: 3 }]}>
790
- selected
790
+ {labels.selectedSuffix}
791
791
  </Text>
792
792
  )}
793
793
  </View>
@@ -851,7 +851,7 @@ export function Select<T extends string | number = string>({
851
851
 
852
852
  <Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
853
853
  <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: asSheet ? colors.overlay : 'transparent', opacity: show }]}>
854
- <Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityLabel="Close" accessibilityRole="button" />
854
+ <Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityLabel={labels.close} accessibilityRole="button" />
855
855
  </Animated.View>
856
856
 
857
857
  {asSheet ? (
@@ -866,8 +866,14 @@ export function Select<T extends string | number = string>({
866
866
  styles.sheet,
867
867
  { backgroundColor: colors.popover, maxHeight: winH * (wide ? 0.7 : 0.78), borderColor: colors.border },
868
868
  wide
869
- ? { width: Math.min(460, winW - 48), borderRadius: radius.xl, paddingBottom: 8 }
870
- : { alignSelf: 'stretch', borderTopLeftRadius: radius.xl + 4, borderTopRightRadius: radius.xl + 4, paddingBottom: 28 },
869
+ ? { width: Math.min(460, layout.sheetWidth), borderRadius: radius.xl, paddingBottom: 8, ...shadow('xl') }
870
+ : {
871
+ alignSelf: 'stretch',
872
+ borderTopLeftRadius: radius.xl + 4,
873
+ borderTopRightRadius: radius.xl + 4,
874
+ paddingBottom: 28,
875
+ boxShadow: `0px -4px 32px rgba(0,0,0,${dark ? 0.45 : 0.18})`,
876
+ },
871
877
  menuStyle,
872
878
  wide
873
879
  ? {
@@ -884,7 +890,7 @@ export function Select<T extends string | number = string>({
884
890
  },
885
891
  ]}
886
892
  >
887
- <View {...pan.panHandlers} style={styles.sheetHeader}>
893
+ <View {...pan} style={styles.sheetHeader}>
888
894
  {!wide ? <View style={[styles.grabber, { backgroundColor: colors.border }]} /> : null}
889
895
  <Text style={[styles.title, ff, { color: colors.foreground }]}>{title ?? label}</Text>
890
896
  {multiple ? (
@@ -892,7 +898,7 @@ export function Select<T extends string | number = string>({
892
898
  {many.length ? (
893
899
  <CountBadge count={many.length} tint={tint} reduce={reduce} />
894
900
  ) : (
895
- <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>
896
902
  )}
897
903
  </View>
898
904
  ) : null}
@@ -912,7 +918,7 @@ export function Select<T extends string | number = string>({
912
918
  backgroundColor: colors.popover,
913
919
  borderColor: colors.border,
914
920
  borderRadius: panelRadius,
915
- shadowOpacity: dark ? 0.5 : 0.12,
921
+ ...shadow('lg'),
916
922
  transformOrigin: below ? 'top' : 'bottom',
917
923
  },
918
924
  menuStyle,
@@ -935,7 +941,7 @@ export function Select<T extends string | number = string>({
935
941
  }
936
942
 
937
943
  /** Groups options by `group` (first-appearance order); a single untitled section when none are grouped. */
938
- 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>[] }[] {
939
945
  if (!list.some((o) => o.group)) return [{ rows: list }];
940
946
  const map = new Map<string, SelectOption<T>[]>();
941
947
  list.forEach((o) => map.set(o.group ?? '', [...(map.get(o.group ?? '') ?? []), o]));
@@ -975,7 +981,7 @@ function OptionRow<T extends string | number>({
975
981
  }) {
976
982
  const { colors, radius, font, dark } = useKit();
977
983
  const ff = font.family ? { fontFamily: font.family } : null;
978
- 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
979
985
  useEffect(() => {
980
986
  // Reduce Motion: the check only fades (no pop / twist).
981
987
  if (reduce) Animated.timing(check, { toValue: selected ? 1 : 0, duration: 150, useNativeDriver: true }).start();
@@ -1075,7 +1081,10 @@ function usePresence<V>(items: { key: string; item: V }[]) {
1075
1081
  const out: Present<V>[] = [];
1076
1082
  let next = 0;
1077
1083
  const emitUpTo = (end: number) => {
1078
- 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
+ }
1079
1088
  };
1080
1089
  for (const e of prev.current) {
1081
1090
  if (live.has(e.key)) emitUpTo(items.findIndex((x) => x.key === e.key));
@@ -1114,11 +1123,13 @@ function AnimatedChip({
1114
1123
  onGone: () => void;
1115
1124
  children: React.ReactNode;
1116
1125
  }) {
1117
- const pop = useRef(new Animated.Value(appear ? 0 : 1)).current; // native: scale + opacity
1118
- 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
1119
1128
  const [w, setW] = useState(0);
1120
1129
  const gone = useRef(onGone);
1121
- gone.current = onGone;
1130
+ useLayoutEffect(() => {
1131
+ gone.current = onGone;
1132
+ });
1122
1133
 
1123
1134
  useEffect(() => {
1124
1135
  let alive = true;
@@ -1231,12 +1242,13 @@ function RollingNumber({ value, style, distance = 10, reduce }: { value: number;
1231
1242
 
1232
1243
  /** "N selected" pill whose number rolls; it pops out when the count drops to 0. */
1233
1244
  function CountBadge({ count, tint, reduce }: { count: number; tint: string; reduce: boolean }) {
1234
- const { radius, font } = useKit();
1245
+ const { radius, font, labels } = useKit();
1235
1246
  const ff = font.family ? { fontFamily: font.family } : null;
1236
1247
  const on = count > 0;
1237
- const vis = useRef(new Animated.Value(on ? 1 : 0)).current; // native
1238
- const last = useRef(count);
1239
- 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);
1240
1252
  useEffect(() => {
1241
1253
  const a = reduce
1242
1254
  ? Animated.timing(vis, { toValue: on ? 1 : 0, duration: 150, useNativeDriver: true })
@@ -1248,7 +1260,7 @@ function CountBadge({ count, tint, reduce }: { count: number; tint: string; redu
1248
1260
  return (
1249
1261
  <Animated.View
1250
1262
  accessible
1251
- accessibilityLabel={`${last.current} selected`}
1263
+ accessibilityLabel={labels.selectedCount(last)}
1252
1264
  accessibilityLiveRegion="polite"
1253
1265
  style={[
1254
1266
  styles.badge,
@@ -1260,9 +1272,9 @@ function CountBadge({ count, tint, reduce }: { count: number; tint: string; redu
1260
1272
  },
1261
1273
  ]}
1262
1274
  >
1263
- <RollingNumber value={last.current} distance={9} reduce={reduce} style={text} />
1275
+ <RollingNumber value={last} distance={9} reduce={reduce} style={text} />
1264
1276
  <Text maxFontSizeMultiplier={FONT_CAP} style={[text, styles.badgeSuffix]}>
1265
- selected
1277
+ {labels.selectedSuffix}
1266
1278
  </Text>
1267
1279
  </Animated.View>
1268
1280
  );
@@ -1290,7 +1302,7 @@ function Highlight({ text, query, tint, style }: { text: string; query: string;
1290
1302
  /** Helper / error line that fades and slides in; errors get an icon and are announced. */
1291
1303
  function FieldMessage({ text, error, reduce, style }: { text: string; error: boolean; reduce: boolean; style?: StyleProp<TextStyle> }) {
1292
1304
  const { colors, font } = useKit();
1293
- const enter = useRef(new Animated.Value(0)).current; // native
1305
+ const [enter] = useState(() => new Animated.Value(0)); // native
1294
1306
  useEffect(() => {
1295
1307
  enter.setValue(0);
1296
1308
  if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
@@ -1312,7 +1324,7 @@ function FieldMessage({ text, error, reduce, style }: { text: string; error: boo
1312
1324
  }
1313
1325
 
1314
1326
  function Chevron({ open, reduce, color, size, right }: { open: boolean; reduce: boolean; color: string; size: number; right: number }) {
1315
- const turn = useRef(new Animated.Value(open ? 1 : 0)).current;
1327
+ const [turn] = useState(() => new Animated.Value(open ? 1 : 0));
1316
1328
  useEffect(() => {
1317
1329
  if (reduce) turn.setValue(open ? 1 : 0);
1318
1330
  else Animated.spring(turn, { toValue: open ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
@@ -1379,7 +1391,7 @@ const styles = StyleSheet.create({
1379
1391
  chip: { flexDirection: 'row', alignItems: 'center', gap: 4, maxWidth: 150, flexShrink: 1 },
1380
1392
  more: { gap: 0, paddingHorizontal: 9, flexShrink: 0 },
1381
1393
  chipIcon: { overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
1382
- chipText: { fontWeight: '400', flexShrink: 1 },
1394
+ chipText: { fontWeight: '400', flexShrink: 1, includeFontPadding: false, textAlignVertical: 'center' },
1383
1395
  tabular: { fontVariant: ['tabular-nums'], fontWeight: '400' },
1384
1396
  roll: { overflow: 'hidden' },
1385
1397
  rollPrev: { position: 'absolute', left: 0, top: 0 },
@@ -1398,11 +1410,6 @@ const styles = StyleSheet.create({
1398
1410
  sheet: {
1399
1411
  paddingHorizontal: 12,
1400
1412
  borderWidth: StyleSheet.hairlineWidth,
1401
- shadowColor: '#000',
1402
- shadowOpacity: 0.18,
1403
- shadowRadius: 24,
1404
- shadowOffset: { width: 0, height: -4 },
1405
- elevation: 16,
1406
1413
  },
1407
1414
  sheetHeader: { alignItems: 'center', paddingTop: 10, paddingBottom: 12 },
1408
1415
  grabber: { width: 40, height: 5, borderRadius: 3, marginBottom: 12 },
@@ -1453,10 +1460,6 @@ const styles = StyleSheet.create({
1453
1460
  dropdown: {
1454
1461
  position: 'absolute',
1455
1462
  borderWidth: StyleSheet.hairlineWidth,
1456
- shadowColor: '#000',
1457
- shadowRadius: 28,
1458
- shadowOffset: { width: 0, height: 10 },
1459
- elevation: 10,
1460
1463
  overflow: 'hidden',
1461
1464
  },
1462
1465
  row: { minHeight: 52, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, gap: 12 },