@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,10 +1,9 @@
1
- import React, { memo, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
1
+ import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
3
  AccessibilityInfo,
4
4
  Animated,
5
5
  Easing,
6
6
  Modal,
7
- PanResponder,
8
7
  Pressable,
9
8
  ScrollView,
10
9
  StyleSheet,
@@ -18,7 +17,7 @@ import {
18
17
  } from 'react-native';
19
18
  import Svg, { Circle, Line, Path } from 'react-native-svg';
20
19
 
21
- import { alpha, motion, readableOn, useKit, useLayout } from '@/lib/kit';
20
+ import { alpha, motion, readableOn, useKit, useLayout, usePanResponder } from '@/lib/kit';
22
21
 
23
22
  /** A wall-clock time, 24-hour: `{ hours: 0–23, minutes: 0–59 }`. */
24
23
  export interface TimeValue {
@@ -143,11 +142,11 @@ export function uses12Hour(locale?: string) {
143
142
  }
144
143
  }
145
144
 
146
- /** '9:30 AM' or '09:30'. */
147
- export function formatTime(t: TimeValue, hour12 = true) {
145
+ /** '9:30 AM' or '09:30'. `periods` names AM / PM (default English; pass `useKit().labels` to localise). */
146
+ export function formatTime(t: TimeValue, hour12 = true, periods: { am: string; pm: string } = { am: 'AM', pm: 'PM' }) {
148
147
  if (!hour12) return `${pad2(t.hours)}:${pad2(t.minutes)}`;
149
148
  const h = t.hours % 12 || 12;
150
- return `${h}:${pad2(t.minutes)} ${t.hours < 12 ? 'AM' : 'PM'}`;
149
+ return `${h}:${pad2(t.minutes)} ${t.hours < 12 ? periods.am : periods.pm}`;
151
150
  }
152
151
 
153
152
  /* ───────────────────────────── TimePicker ───────────────────────────── */
@@ -194,18 +193,18 @@ interface WheelProps {
194
193
  step: number;
195
194
  value: TimeValue | null;
196
195
  onChange: (t: TimeValue) => void;
197
- accent?: string;
198
- style?: StyleProp<ViewStyle>;
196
+ accent?: string | undefined;
197
+ style?: StyleProp<ViewStyle> | undefined;
199
198
  reduce: boolean;
200
199
  }
201
200
 
202
201
  function Wheel({ hour12, step, value, onChange, accent, style, reduce }: WheelProps) {
203
- const { colors, radius, font, dark } = useKit();
202
+ const { colors, radius, font, dark, labels } = useKit();
204
203
  const tint = accent ?? colors.accent;
205
204
  const t = value ?? { hours: 9, minutes: 0 };
206
205
  const hours = useMemo(() => (hour12 ? ['12', ...Array.from({ length: 11 }, (_, i) => String(i + 1))] : Array.from({ length: 24 }, (_, i) => pad2(i))), [hour12]);
207
206
  const minutes = useMemo(() => Array.from({ length: Math.ceil(60 / step) }, (_, i) => pad2(i * step)), [step]);
208
- const periods = useMemo(() => ['AM', 'PM'], []);
207
+ const periods = useMemo(() => [labels.am, labels.pm], [labels.am, labels.pm]);
209
208
 
210
209
  const hIdx = hour12 ? t.hours % 12 : t.hours;
211
210
  const mIdx = Math.min(minutes.length - 1, Math.round(t.minutes / step));
@@ -213,7 +212,9 @@ function Wheel({ hour12, step, value, onChange, accent, style, reduce }: WheelPr
213
212
 
214
213
  // Keep the latest value for column callbacks that fire after a scroll settles.
215
214
  const latest = useRef(t);
216
- latest.current = t;
215
+ useLayoutEffect(() => {
216
+ latest.current = t;
217
+ });
217
218
  const set = (patch: Partial<{ h: number; m: number; p: number }>) => {
218
219
  const cur = latest.current;
219
220
  const curP = cur.hours >= 12 ? 1 : 0;
@@ -233,17 +234,17 @@ function Wheel({ hour12, step, value, onChange, accent, style, reduce }: WheelPr
233
234
  style={[styles.band, { top: PAD, height: ITEM, borderRadius: radius.scale(12), backgroundColor: alpha(tint, dark ? 0.16 : 0.11) }]}
234
235
  />
235
236
  <View style={styles.wheelGroup}>
236
- <WheelColumn items={hours} index={hIdx} onIndexChange={(h) => set({ h })} label="Hour" width={64} align="right" reduce={reduce} />
237
+ <WheelColumn items={hours} index={hIdx} onIndexChange={(h) => set({ h })} label={labels.hour} width={64} align="right" reduce={reduce} />
237
238
  <View pointerEvents="none" style={styles.colon}>
238
239
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.colonText, font.family ? { fontFamily: font.family } : null, { color: colors.mutedForeground }]}>:</Text>
239
240
  </View>
240
- <WheelColumn items={minutes} index={mIdx} onIndexChange={(m) => set({ m })} label="Minute" width={64} align="left" reduce={reduce} />
241
+ <WheelColumn items={minutes} index={mIdx} onIndexChange={(m) => set({ m })} label={labels.minute} width={64} align="left" reduce={reduce} />
241
242
  {hour12 ? (
242
243
  <WheelColumn
243
244
  items={periods}
244
245
  index={pIdx}
245
246
  onIndexChange={(p) => set({ p })}
246
- label="AM or PM"
247
+ label={labels.amPm}
247
248
  width={60}
248
249
  align="center"
249
250
  small
@@ -272,17 +273,21 @@ interface WheelColumnProps {
272
273
  const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, label, width, align, small, style, reduce }: WheelColumnProps) {
273
274
  const { colors, font } = useKit();
274
275
  const ff = font.family ? { fontFamily: font.family } : null;
275
- const scrollY = useRef(new Animated.Value(index * ITEM)).current; // native, driven by the scroll view
276
+ const [scrollY] = useState(() => new Animated.Value(index * ITEM)); // native, driven by the scroll view
276
277
  const ref = useRef<ComponentRef<typeof ScrollView>>(null);
277
278
  const settled = useRef(index);
278
279
  const dragging = useRef(false);
279
280
  const lastY = useRef(index * ITEM);
280
281
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
281
282
  const cb = useRef(onIndexChange);
282
- cb.current = onIndexChange;
283
+ useLayoutEffect(() => {
284
+ cb.current = onIndexChange;
285
+ });
283
286
  // Read by settle(), which also runs from the scroll listener created once.
284
287
  const animated = useRef(!reduce);
285
- animated.current = !reduce;
288
+ useLayoutEffect(() => {
289
+ animated.current = !reduce;
290
+ });
286
291
 
287
292
  useEffect(() => {
288
293
  if (index !== settled.current && !dragging.current) {
@@ -316,6 +321,7 @@ const WheelColumn = memo(function WheelColumn({ items, index, onIndexChange, lab
316
321
 
317
322
  const onScroll = useMemo(
318
323
  () =>
324
+ // eslint-disable-next-line react-hooks/refs -- the listener reads refs on scroll events, never during render
319
325
  Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
320
326
  useNativeDriver: true,
321
327
  listener: (e: NativeSyntheticEvent<NativeScrollEvent>) => {
@@ -402,8 +408,8 @@ interface ClockProps {
402
408
  step: number;
403
409
  value: TimeValue | null;
404
410
  onChange: (t: TimeValue) => void;
405
- accent?: string;
406
- style?: StyleProp<ViewStyle>;
411
+ accent?: string | undefined;
412
+ style?: StyleProp<ViewStyle> | undefined;
407
413
  reduce: boolean;
408
414
  }
409
415
 
@@ -423,7 +429,7 @@ const handAngle = (t: TimeValue, mode: ClockMode) => (mode === 'hour' ? (t.hours
423
429
  * native driver and snaps to hours, then auto-advances to minutes when you let go.
424
430
  */
425
431
  function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockProps) {
426
- const { colors, radius, font, dark } = useKit();
432
+ const { colors, radius, font, dark, labels } = useKit();
427
433
  const ff = font.family ? { fontFamily: font.family } : null;
428
434
  const tint = accent ?? colors.accent;
429
435
  const ink = readableOn(tint);
@@ -441,9 +447,9 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
441
447
  const handR = mode === 'hour' && innerHour ? inner : outer;
442
448
 
443
449
  // Hand rotation in degrees (native). Kept continuous so 55 → 00 turns forward, not all the way back.
444
- const rot = useRef(new Animated.Value(handAngle(t, 'hour'))).current;
450
+ const [rot] = useState(() => new Animated.Value(handAngle(t, 'hour')));
445
451
  const angle = useRef(handAngle(t, 'hour'));
446
- const modeAnim = useRef(new Animated.Value(0)).current; // native: number crossfade
452
+ const [modeAnim] = useState(() => new Animated.Value(0)); // native: number crossfade
447
453
  useEffect(() => {
448
454
  const target = handAngle(t, mode);
449
455
  const d = ((((target - angle.current) % 360) + 540) % 360) - 180;
@@ -459,48 +465,48 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
459
465
 
460
466
  // Latest values for the pan responder, which is created once.
461
467
  const live = useRef({ t, mode, hour12, step, onChange, c, outer, inner, touched: value !== null });
462
- live.current = { t, mode, hour12, step, onChange, c, outer, inner, touched: value !== null };
468
+ useLayoutEffect(() => {
469
+ live.current = { t, mode, hour12, step, onChange, c, outer, inner, touched: value !== null };
470
+ });
463
471
  const origin = useRef({ x: 0, y: 0 });
464
472
 
465
- const pan = useMemo(() => {
466
- const pick = (x: number, y: number) => {
467
- const L = live.current;
468
- const dx = x - L.c;
469
- const dy = y - L.c;
470
- let deg = (Math.atan2(dx, -dy) * 180) / Math.PI;
471
- if (deg < 0) deg += 360;
472
- const cur = L.t;
473
- let next: TimeValue;
474
- if (L.mode === 'hour') {
475
- const i = Math.round(deg / 30) % 12;
476
- if (L.hour12) next = { hours: i + (cur.hours >= 12 ? 12 : 0), minutes: cur.minutes };
477
- else {
478
- const innerRing = Math.hypot(dx, dy) < (L.outer + L.inner) / 2;
479
- next = { hours: innerRing ? (i === 0 ? 0 : i + 12) : i === 0 ? 12 : i, minutes: cur.minutes };
480
- }
481
- } else {
482
- next = { hours: cur.hours, minutes: (Math.round(deg / 6 / L.step) * L.step) % 60 };
473
+ const pick = (x: number, y: number) => {
474
+ const L = live.current;
475
+ const dx = x - L.c;
476
+ const dy = y - L.c;
477
+ let deg = (Math.atan2(dx, -dy) * 180) / Math.PI;
478
+ if (deg < 0) deg += 360;
479
+ const cur = L.t;
480
+ let next: TimeValue;
481
+ if (L.mode === 'hour') {
482
+ const i = Math.round(deg / 30) % 12;
483
+ if (L.hour12) next = { hours: i + (cur.hours >= 12 ? 12 : 0), minutes: cur.minutes };
484
+ else {
485
+ const innerRing = Math.hypot(dx, dy) < (L.outer + L.inner) / 2;
486
+ next = { hours: innerRing ? (i === 0 ? 0 : i + 12) : i === 0 ? 12 : i, minutes: cur.minutes };
483
487
  }
484
- if (next.hours !== cur.hours || next.minutes !== cur.minutes || !L.touched) {
485
- L.t = next;
486
- L.touched = true;
487
- L.onChange(next);
488
- }
489
- };
490
- return PanResponder.create({
491
- onStartShouldSetPanResponder: () => true,
492
- onMoveShouldSetPanResponder: () => true,
493
- onPanResponderTerminationRequest: () => false,
494
- onPanResponderGrant: (e) => {
495
- origin.current = { x: e.nativeEvent.locationX, y: e.nativeEvent.locationY };
496
- pick(origin.current.x, origin.current.y);
497
- },
498
- onPanResponderMove: (_, g) => pick(origin.current.x + g.dx, origin.current.y + g.dy),
499
- onPanResponderRelease: () => {
500
- if (live.current.mode === 'hour') setMode('minute');
501
- },
502
- });
503
- }, []);
488
+ } else {
489
+ next = { hours: cur.hours, minutes: (Math.round(deg / 6 / L.step) * L.step) % 60 };
490
+ }
491
+ if (next.hours !== cur.hours || next.minutes !== cur.minutes || !L.touched) {
492
+ L.t = next;
493
+ L.touched = true;
494
+ L.onChange(next);
495
+ }
496
+ };
497
+ const pan = usePanResponder({
498
+ onStartShouldSetPanResponder: () => true,
499
+ onMoveShouldSetPanResponder: () => true,
500
+ onPanResponderTerminationRequest: () => false,
501
+ onPanResponderGrant: (e) => {
502
+ origin.current = { x: e.nativeEvent.locationX, y: e.nativeEvent.locationY };
503
+ pick(origin.current.x, origin.current.y);
504
+ },
505
+ onPanResponderMove: (_, g) => pick(origin.current.x + g.dx, origin.current.y + g.dy),
506
+ onPanResponderRelease: () => {
507
+ if (live.current.mode === 'hour') setMode('minute');
508
+ },
509
+ });
504
510
 
505
511
  const adjust = (dir: 1 | -1) => {
506
512
  if (mode === 'hour') {
@@ -554,22 +560,22 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
554
560
  return (
555
561
  <View style={[styles.clock, style]} onLayout={(e) => setWidth(e.nativeEvent.layout.width)}>
556
562
  <View style={styles.clockHead}>
557
- {segment('hour', hourText, 'Hour')}
563
+ {segment('hour', hourText, labels.hour)}
558
564
  <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.clockColon, ff, { fontSize: segFont, color: colors.mutedForeground }]}>:</Text>
559
- {segment('minute', pad2(t.minutes), 'Minute')}
565
+ {segment('minute', pad2(t.minutes), labels.minute)}
560
566
  {hour12 ? (
561
567
  <View
562
568
  accessibilityRole="radiogroup"
563
569
  style={[styles.period, { height: segH, borderRadius: radius.scale(10), borderColor: colors.border }]}
564
570
  >
565
- {(['AM', 'PM'] as const).map((p, i) => {
566
- const on = (p === 'PM') === pm;
571
+ {(['am', 'pm'] as const).map((p, i) => {
572
+ const on = (p === 'pm') === pm;
567
573
  return (
568
574
  <Pressable
569
575
  key={p}
570
- onPress={() => setPeriod(p === 'PM')}
576
+ onPress={() => setPeriod(p === 'pm')}
571
577
  accessibilityRole="radio"
572
- accessibilityLabel={p}
578
+ accessibilityLabel={labels[p]}
573
579
  accessibilityState={{ checked: on }}
574
580
  style={[
575
581
  styles.periodBtn,
@@ -577,7 +583,7 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
577
583
  i === 1 && { borderTopWidth: StyleSheet.hairlineWidth * 2, borderTopColor: colors.border },
578
584
  ]}
579
585
  >
580
- <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.periodText, ff, { color: on ? tint : colors.mutedForeground }]}>{p}</Text>
586
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.periodText, ff, { color: on ? tint : colors.mutedForeground }]}>{labels[p]}</Text>
581
587
  </Pressable>
582
588
  );
583
589
  })}
@@ -586,12 +592,12 @@ function Clock({ hour12, step, value, onChange, accent, style, reduce }: ClockPr
586
592
  </View>
587
593
 
588
594
  <View
589
- {...pan.panHandlers}
595
+ {...pan}
590
596
  accessible
591
597
  accessibilityRole="adjustable"
592
- accessibilityLabel={mode === 'hour' ? 'Hour' : 'Minute'}
593
- accessibilityHint="Drag around the dial, or swipe up or down to adjust."
594
- accessibilityValue={{ text: mode === 'hour' ? `${knobLabel || hourText} ${hour12 ? (pm ? 'PM' : 'AM') : 'hours'}` : `${t.minutes} minutes` }}
598
+ accessibilityLabel={mode === 'hour' ? labels.hour : labels.minute}
599
+ accessibilityHint={labels.timeDialHint}
600
+ accessibilityValue={{ text: mode === 'hour' ? (hour12 ? `${knobLabel || hourText} ${pm ? labels.pm : labels.am}` : labels.hours(knobLabel || hourText)) : labels.minutes(t.minutes) }}
595
601
  accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
596
602
  onAccessibilityAction={(e) => adjust(e.nativeEvent.actionName === 'increment' ? 1 : -1)}
597
603
  style={[styles.face, { width: size, height: size }]}
@@ -666,7 +672,7 @@ function SlotGrid({
666
672
  onPick,
667
673
  reduce,
668
674
  }: SlotGridProps) {
669
- const { colors, font } = useKit();
675
+ const { colors, font, labels } = useKit();
670
676
  const ff = font.family ? { fontFamily: font.family } : null;
671
677
  const tint = accent ?? colors.accent;
672
678
  const list = useMemo(() => {
@@ -682,12 +688,12 @@ function SlotGrid({
682
688
  const groups = useMemo(() => {
683
689
  if (!groupByPeriod) return [{ title: '', items: list }];
684
690
  const g = [
685
- { title: 'Morning', items: list.filter((n) => n < 720) },
686
- { title: 'Afternoon', items: list.filter((n) => n >= 720 && n < 1020) },
687
- { title: 'Evening', items: list.filter((n) => n >= 1020) },
691
+ { title: labels.morning, items: list.filter((n) => n < 720) },
692
+ { title: labels.afternoon, items: list.filter((n) => n >= 720 && n < 1020) },
693
+ { title: labels.evening, items: list.filter((n) => n >= 1020) },
688
694
  ];
689
695
  return g.filter((x) => x.items.length);
690
- }, [list, groupByPeriod]);
696
+ }, [list, groupByPeriod, labels]);
691
697
 
692
698
  return (
693
699
  <View style={[styles.slots, style]} accessibilityRole="radiogroup">
@@ -698,7 +704,7 @@ function SlotGrid({
698
704
  {g.title ? (
699
705
  <View style={styles.groupHead}>
700
706
  <Text style={[styles.groupTitle, ff, { color: colors.foreground }]}>{g.title}</Text>
701
- <Text style={[styles.groupCount, ff, { color: colors.mutedForeground }]}>{free ? `${free} open` : 'Full'}</Text>
707
+ <Text style={[styles.groupCount, ff, { color: colors.mutedForeground }]}>{free ? labels.slotsOpen(free) : labels.full}</Text>
702
708
  </View>
703
709
  ) : null}
704
710
  <View style={styles.grid}>
@@ -706,7 +712,7 @@ function SlotGrid({
706
712
  <View key={n} style={[styles.chipCell, { width: `${100 / columns}%` }]}>
707
713
  <Chip
708
714
  minutes={n}
709
- label={formatTime(fromMin(n), hour12)}
715
+ label={formatTime(fromMin(n), hour12, labels)}
710
716
  selected={n === selected}
711
717
  booked={booked.has(n)}
712
718
  disabled={off.has(n)}
@@ -737,9 +743,9 @@ interface ChipProps {
737
743
  }
738
744
 
739
745
  const Chip = memo(function Chip({ minutes, label, selected, booked, disabled, tint, reduce, onPick }: ChipProps) {
740
- const { colors, radius, font } = useKit();
741
- const pop = useRef(new Animated.Value(1)).current; // native: press + select spring
742
- const on = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: fill opacity
746
+ const { colors, radius, font, labels } = useKit();
747
+ const [pop] = useState(() => new Animated.Value(1)); // native: press + select spring
748
+ const [on] = useState(() => new Animated.Value(selected ? 1 : 0)); // native: fill opacity
743
749
  useEffect(() => {
744
750
  Animated.timing(on, { toValue: selected ? 1 : 0, duration: motion.fast, useNativeDriver: true }).start();
745
751
  if (selected && !reduce) {
@@ -756,7 +762,7 @@ const Chip = memo(function Chip({ minutes, label, selected, booked, disabled, ti
756
762
  onPressOut={reduce ? undefined : () => Animated.spring(pop, { toValue: 1, useNativeDriver: true, ...motion.bouncy }).start()}
757
763
  disabled={blocked}
758
764
  accessibilityRole="radio"
759
- accessibilityLabel={`${label}${booked ? ', booked' : disabled ? ', unavailable' : ''}`}
765
+ accessibilityLabel={`${label}${booked ? `, ${labels.booked}` : disabled ? `, ${labels.unavailable}` : ''}`}
760
766
  accessibilityState={{ checked: selected, disabled: blocked }}
761
767
  >
762
768
  <Animated.View
@@ -806,10 +812,10 @@ function TimeField({
806
812
  current,
807
813
  commit,
808
814
  reduce,
809
- label = 'Time',
815
+ label: labelProp,
810
816
  floatingLabel = true,
811
817
  title,
812
- placeholder = 'Select a time',
818
+ placeholder: placeholderProp,
813
819
  size = 'md',
814
820
  confirm,
815
821
  error,
@@ -828,7 +834,9 @@ function TimeField({
828
834
  menuStyle,
829
835
  ...chipProps
830
836
  }: TimeFieldProps) {
831
- const { colors, radius, font, dark, shadow } = useKit();
837
+ const { colors, radius, font, dark, shadow, labels } = useKit();
838
+ const label = labelProp ?? labels.time;
839
+ const placeholder = placeholderProp ?? labels.selectTime;
832
840
  const ff = font.family ? { fontFamily: font.family } : null;
833
841
  const tint = accent ?? colors.accent;
834
842
  const bg = surface ?? colors.background;
@@ -849,19 +857,16 @@ function TimeField({
849
857
  const [mounted, setMounted] = useState(false);
850
858
  const [draft, setDraft] = useState<TimeValue | null>(current);
851
859
  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
852
- // Read by the sheet's pan responder, which is created once.
853
- const reduceRef = useRef(reduce);
854
- reduceRef.current = reduce;
855
860
 
856
861
  const ok = !error && !!success;
857
862
  const floating = open || !!current;
858
- const float = useRef(new Animated.Value(floating ? 1 : 0)).current; // native
859
- const focus = useRef(new Animated.Value(0)).current; // JS: colours
860
- const halo = useRef(new Animated.Value(0)).current; // native
861
- const show = useRef(new Animated.Value(0)).current; // native
862
- const drag = useRef(new Animated.Value(0)).current; // native
863
- const shake = useRef(new Animated.Value(0)).current; // native
864
- const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
863
+ const [float] = useState(() => new Animated.Value(floating ? 1 : 0)); // native
864
+ const [focus] = useState(() => new Animated.Value(0)); // JS: colours
865
+ const [halo] = useState(() => new Animated.Value(0)); // native
866
+ const [show] = useState(() => new Animated.Value(0)); // native
867
+ const [drag] = useState(() => new Animated.Value(0)); // native
868
+ const [shake] = useState(() => new Animated.Value(0)); // native
869
+ const [check] = useState(() => new Animated.Value(ok ? 1 : 0)); // native
865
870
 
866
871
  useEffect(() => {
867
872
  if (reduce) float.setValue(floating ? 1 : 0);
@@ -918,21 +923,19 @@ function TimeField({
918
923
  }
919
924
  };
920
925
 
921
- const pan = useMemo(() => {
922
- const back = () =>
923
- reduceRef.current
924
- ? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
925
- : Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
926
- return PanResponder.create({
927
- onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
928
- onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
929
- onPanResponderRelease: (_, g) => {
930
- if (g.dy > 110 || g.vy > 1.1) setOpen(false);
931
- else back();
932
- },
933
- onPanResponderTerminate: back,
934
- });
935
- }, [drag]);
926
+ const back = () =>
927
+ reduce
928
+ ? Animated.timing(drag, { toValue: 0, duration: 150, easing: Easing.bezier(0.32, 0.72, 0, 1), useNativeDriver: true }).start()
929
+ : Animated.spring(drag, { toValue: 0, useNativeDriver: true, ...motion.snappy }).start();
930
+ const pan = usePanResponder({
931
+ onMoveShouldSetPanResponder: (_, g) => g.dy > 4 && Math.abs(g.dy) > Math.abs(g.dx),
932
+ onPanResponderMove: (_, g) => drag.setValue(Math.max(0, g.dy)),
933
+ onPanResponderRelease: (_, g) => {
934
+ if (g.dy > 110 || g.vy > 1.1) setOpen(false);
935
+ else back();
936
+ },
937
+ onPanResponderTerminate: back,
938
+ });
936
939
 
937
940
  const active = error ? colors.danger : tint;
938
941
  const rest = error ? colors.danger : ok ? colors.success : ((userBorder as string | undefined) ?? colors.input);
@@ -942,7 +945,7 @@ function TimeField({
942
945
  outputRange: [error ? colors.danger : ok ? colors.success : (userLabelColor ?? colors.mutedForeground), active],
943
946
  });
944
947
  const topLabelColor = focus.interpolate({ inputRange: [0, 1], outputRange: [error ? colors.danger : (userLabelColor ?? colors.foreground), active] });
945
- const text = current ? formatTime(current, hour12) : '';
948
+ const text = current ? formatTime(current, hour12, labels) : '';
946
949
  const successText = typeof success === 'string' && ok ? success : undefined;
947
950
  const message = error ?? successText ?? helper;
948
951
  const tone = error ? 'error' : successText ? 'success' : 'helper';
@@ -966,7 +969,7 @@ function TimeField({
966
969
  disabled={disabled}
967
970
  accessibilityRole="button"
968
971
  accessibilityLabel={label}
969
- accessibilityValue={{ text: text || 'Not set' }}
972
+ accessibilityValue={{ text: text || labels.notSet }}
970
973
  accessibilityState={{ expanded: open, disabled: !!disabled }}
971
974
  style={{ opacity: disabled ? 0.5 : 1 }}
972
975
  >
@@ -1032,7 +1035,7 @@ function TimeField({
1032
1035
 
1033
1036
  <Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={close}>
1034
1037
  <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: colors.overlay, opacity: show }]}>
1035
- <Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityLabel="Close" accessibilityRole="button" />
1038
+ <Pressable style={StyleSheet.absoluteFill} onPress={close} accessibilityLabel={labels.close} accessibilityRole="button" />
1036
1039
  </Animated.View>
1037
1040
  <View style={[styles.sheetHost, wide && styles.centered]} pointerEvents="box-none">
1038
1041
  <Animated.View
@@ -1065,11 +1068,11 @@ function TimeField({
1065
1068
  },
1066
1069
  ]}
1067
1070
  >
1068
- <View {...pan.panHandlers} style={styles.sheetHeader}>
1071
+ <View {...pan} style={styles.sheetHeader}>
1069
1072
  {!wide ? <View style={[styles.grabber, { backgroundColor: colors.border }]} /> : null}
1070
1073
  <Text style={[styles.title, ff, { color: colors.foreground }]}>{title ?? label}</Text>
1071
1074
  {variant === 'clock' ? null : (
1072
- <Text style={[styles.preview, ff, { color: draft ? tint : colors.mutedForeground }]}>{draft ? formatTime(draft, hour12) : placeholder}</Text>
1075
+ <Text style={[styles.preview, ff, { color: draft ? tint : colors.mutedForeground }]}>{draft ? formatTime(draft, hour12, labels) : placeholder}</Text>
1073
1076
  )}
1074
1077
  </View>
1075
1078
  <ScrollView style={styles.sheetBody} contentContainerStyle={styles.sheetContent} bounces={false} scrollEnabled={variant === 'chips'}>
@@ -1084,7 +1087,7 @@ function TimeField({
1084
1087
  {staged ? (
1085
1088
  <View style={[styles.footer, { borderTopColor: colors.border }]}>
1086
1089
  <Pressable onPress={close} accessibilityRole="button" style={({ pressed }) => [styles.footBtn, { borderRadius: radius.md, opacity: pressed ? 0.6 : 1 }]}>
1087
- <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>Cancel</Text>
1090
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: colors.foreground }]}>{labels.cancel}</Text>
1088
1091
  </Pressable>
1089
1092
  <Pressable
1090
1093
  onPress={() => {
@@ -1099,7 +1102,7 @@ function TimeField({
1099
1102
  { backgroundColor: tint, borderRadius: radius.md, opacity: !draft ? 0.4 : pressed ? 0.85 : 1 },
1100
1103
  ]}
1101
1104
  >
1102
- <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: readableOn(tint) }]}>{draft ? `Set ${formatTime(draft, hour12)}` : 'Done'}</Text>
1105
+ <Text maxFontSizeMultiplier={FONT_CAP} style={[styles.footText, ff, { color: readableOn(tint) }]}>{draft ? labels.setTime(formatTime(draft, hour12, labels)) : labels.done}</Text>
1103
1106
  </Pressable>
1104
1107
  </View>
1105
1108
  ) : null}
@@ -1123,7 +1126,7 @@ function FieldMessage({
1123
1126
  style?: StyleProp<TextStyle>;
1124
1127
  }) {
1125
1128
  const { colors, font } = useKit();
1126
- const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
1129
+ const [enter] = useState(() => new Animated.Value(tone === 'helper' ? 1 : 0)); // native
1127
1130
  useEffect(() => {
1128
1131
  if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
1129
1132
  else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();