@expokit/cli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +8 -4
  2. package/bin/expo-kit.js +137 -24
  3. package/bin/lib.mjs +110 -1
  4. package/bin/mcp.mjs +40 -18
  5. package/package.json +2 -2
  6. package/registry/_docs/accordion.json +1 -1
  7. package/registry/_docs/assistant-app.json +47 -0
  8. package/registry/_docs/avatar.json +6 -6
  9. package/registry/_docs/badge.json +4 -4
  10. package/registry/_docs/bottom-nav.json +6 -6
  11. package/registry/_docs/bottom-sheet.json +3 -3
  12. package/registry/_docs/bullet.json +6 -0
  13. package/registry/_docs/button-3d.json +3 -3
  14. package/registry/_docs/button.json +2 -2
  15. package/registry/_docs/calendar.json +5 -5
  16. package/registry/_docs/card.json +1 -1
  17. package/registry/_docs/checkout.json +7 -1
  18. package/registry/_docs/chip-group.json +1 -1
  19. package/registry/_docs/confetti.json +3 -3
  20. package/registry/_docs/date-picker.json +4 -4
  21. package/registry/_docs/donut-shop.json +88 -0
  22. package/registry/_docs/drawer.json +4 -4
  23. package/registry/_docs/fintech-app.json +49 -0
  24. package/registry/_docs/flip-card.json +4 -4
  25. package/registry/_docs/floating-input.json +5 -5
  26. package/registry/_docs/food-app.json +47 -0
  27. package/registry/_docs/glass.json +1 -1
  28. package/registry/_docs/grain.json +80 -0
  29. package/registry/_docs/isometric-bar.json +6 -0
  30. package/registry/_docs/liquidglass-tab.json +1 -1
  31. package/registry/_docs/morphing-button-group.json +96 -0
  32. package/registry/_docs/orb-reveal.json +132 -0
  33. package/registry/_docs/otp-input.json +4 -4
  34. package/registry/_docs/password-input.json +3 -3
  35. package/registry/_docs/payment-card.json +3 -3
  36. package/registry/_docs/pixel-bar.json +6 -0
  37. package/registry/_docs/polar-area.json +6 -0
  38. package/registry/_docs/progress-steps.json +4 -4
  39. package/registry/_docs/progress.json +2 -2
  40. package/registry/_docs/qr-code-button.json +122 -0
  41. package/registry/_docs/radio-group.json +1 -1
  42. package/registry/_docs/search-bar.json +4 -4
  43. package/registry/_docs/segmented-control.json +1 -1
  44. package/registry/_docs/select.json +7 -7
  45. package/registry/_docs/settings.json +6 -0
  46. package/registry/_docs/skeleton.json +3 -3
  47. package/registry/_docs/slider.json +6 -6
  48. package/registry/_docs/spinner.json +3 -3
  49. package/registry/_docs/stateful-button.json +1 -1
  50. package/registry/_docs/stepper.json +7 -7
  51. package/registry/_docs/tabs.json +2 -2
  52. package/registry/_docs/text-animation.json +4 -4
  53. package/registry/_docs/textarea.json +3 -3
  54. package/registry/_docs/ticket-shape.json +1 -1
  55. package/registry/_docs/tilt-card.json +4 -4
  56. package/registry/_docs/time-picker.json +6 -6
  57. package/registry/_docs/timeline-chart.json +6 -0
  58. package/registry/_docs/timeline.json +6 -6
  59. package/registry/_docs/toast.json +8 -8
  60. package/registry/_docs/treemap.json +6 -0
  61. package/registry/_docs/waterfall.json +6 -0
  62. package/registry/accordion/files/src/components/ui/accordion.tsx +36 -7
  63. package/registry/accordion/files/src/lib/kit.tsx +8 -5
  64. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +12 -8
  65. package/registry/action-sheet/files/src/lib/kit.tsx +8 -5
  66. package/registry/alert/files/src/components/ui/alert.tsx +11 -5
  67. package/registry/alert/files/src/components/ui/button.tsx +35 -6
  68. package/registry/alert/files/src/lib/kit.tsx +8 -5
  69. package/registry/assistant-app.json +49 -0
  70. package/registry/avatar/files/src/components/ui/avatar.tsx +7 -4
  71. package/registry/avatar/files/src/lib/kit.tsx +8 -5
  72. package/registry/badge/files/src/components/ui/badge.tsx +23 -9
  73. package/registry/badge/files/src/lib/kit.tsx +8 -5
  74. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +21 -8
  75. package/registry/bottom-nav/files/src/lib/kit.tsx +8 -5
  76. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +7 -6
  77. package/registry/bottom-sheet/files/src/lib/kit.tsx +8 -5
  78. package/registry/button/files/src/components/ui/button.tsx +35 -6
  79. package/registry/button/files/src/lib/kit.tsx +8 -5
  80. package/registry/button-3d/files/src/components/ui/button-3d.tsx +35 -7
  81. package/registry/button-3d/files/src/lib/kit.tsx +8 -5
  82. package/registry/calendar/files/src/components/ui/calendar.tsx +68 -19
  83. package/registry/calendar/files/src/lib/kit.tsx +8 -5
  84. package/registry/card/files/src/components/ui/card.tsx +32 -10
  85. package/registry/card/files/src/lib/kit.tsx +8 -5
  86. package/registry/checkbox/files/src/components/ui/checkbox.tsx +31 -8
  87. package/registry/checkbox/files/src/lib/kit.tsx +8 -5
  88. package/registry/chip-group/files/src/components/ui/chip-group.tsx +39 -7
  89. package/registry/chip-group/files/src/lib/kit.tsx +8 -5
  90. package/registry/confetti/files/src/lib/kit.tsx +8 -5
  91. package/registry/coupon/files/src/components/ui/coupon.tsx +55 -28
  92. package/registry/coupon/files/src/components/ui/flip-card.tsx +23 -4
  93. package/registry/coupon/files/src/components/ui/tilt-card.tsx +59 -18
  94. package/registry/coupon/files/src/lib/kit.tsx +8 -5
  95. package/registry/date-picker/files/src/components/ui/calendar.tsx +68 -19
  96. package/registry/date-picker/files/src/components/ui/date-picker.tsx +98 -44
  97. package/registry/date-picker/files/src/lib/kit.tsx +8 -5
  98. package/registry/dialog/files/src/components/ui/dialog.tsx +19 -9
  99. package/registry/dialog/files/src/lib/kit.tsx +8 -5
  100. package/registry/donut-shop.json +18 -0
  101. package/registry/drawer/files/src/components/ui/drawer.tsx +31 -21
  102. package/registry/drawer/files/src/lib/kit.tsx +8 -5
  103. package/registry/fintech-app.json +57 -0
  104. package/registry/flip-card/files/src/components/ui/flip-card.tsx +23 -4
  105. package/registry/flip-card/files/src/lib/kit.tsx +8 -5
  106. package/registry/floating-input/files/src/components/ui/floating-input.tsx +91 -32
  107. package/registry/floating-input/files/src/lib/kit.tsx +8 -5
  108. package/registry/food-app.json +55 -0
  109. package/registry/grain/files/src/components/ui/grain.tsx +113 -0
  110. package/registry/{tag-input → grain}/files/src/lib/kit.tsx +8 -5
  111. package/registry/grain.json +17 -0
  112. package/registry/kit/files/src/lib/kit.tsx +8 -5
  113. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +2 -2
  114. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +1 -1
  115. package/registry/list-item/files/src/components/ui/list-item.tsx +28 -17
  116. package/registry/list-item/files/src/lib/kit.tsx +8 -5
  117. package/registry/morphing-button-group.json +18 -0
  118. package/registry/orb-reveal.json +20 -0
  119. package/registry/otp-input/files/src/components/ui/otp-input.tsx +57 -24
  120. package/registry/otp-input/files/src/lib/kit.tsx +8 -5
  121. package/registry/password-input/files/src/components/ui/floating-input.tsx +91 -32
  122. package/registry/password-input/files/src/components/ui/password-input.tsx +55 -21
  123. package/registry/password-input/files/src/lib/kit.tsx +8 -5
  124. package/registry/payment-card/files/src/components/ui/flip-card.tsx +23 -4
  125. package/registry/payment-card/files/src/components/ui/payment-card.tsx +35 -18
  126. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +59 -18
  127. package/registry/payment-card/files/src/lib/kit.tsx +8 -5
  128. package/registry/progress/files/src/components/ui/progress.tsx +16 -6
  129. package/registry/progress/files/src/lib/kit.tsx +8 -5
  130. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +22 -11
  131. package/registry/progress-steps/files/src/lib/kit.tsx +8 -5
  132. package/registry/qr-code-button.json +18 -0
  133. package/registry/radio-group/files/src/components/ui/radio-group.tsx +48 -17
  134. package/registry/radio-group/files/src/lib/kit.tsx +8 -5
  135. package/registry/search-bar/files/src/components/ui/search-bar.tsx +81 -26
  136. package/registry/search-bar/files/src/lib/kit.tsx +8 -5
  137. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +40 -10
  138. package/registry/segmented-control/files/src/lib/kit.tsx +8 -5
  139. package/registry/select/files/src/components/ui/select.tsx +161 -86
  140. package/registry/select/files/src/lib/kit.tsx +8 -5
  141. package/registry/skeleton/files/src/components/ui/skeleton.tsx +4 -4
  142. package/registry/skeleton/files/src/lib/kit.tsx +8 -5
  143. package/registry/slider/files/src/components/ui/slider.tsx +56 -27
  144. package/registry/slider/files/src/lib/kit.tsx +8 -5
  145. package/registry/spinner/files/src/components/ui/spinner.tsx +17 -12
  146. package/registry/spinner/files/src/lib/kit.tsx +8 -5
  147. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +12 -4
  148. package/registry/stateful-button/files/src/lib/kit.tsx +8 -5
  149. package/registry/stepper/files/src/components/ui/stepper.tsx +83 -29
  150. package/registry/stepper/files/src/lib/kit.tsx +8 -5
  151. package/registry/switch/files/src/components/ui/switch.tsx +60 -22
  152. package/registry/switch/files/src/lib/kit.tsx +8 -5
  153. package/registry/tabs/files/src/components/ui/tabs.tsx +15 -10
  154. package/registry/tabs/files/src/lib/kit.tsx +8 -5
  155. package/registry/text-animation/files/src/components/ui/text-animation.tsx +11 -6
  156. package/registry/text-animation/files/src/lib/kit.tsx +8 -5
  157. package/registry/textarea/files/src/components/ui/textarea.tsx +115 -45
  158. package/registry/textarea/files/src/lib/kit.tsx +8 -5
  159. package/registry/ticket-shape/files/src/lib/kit.tsx +8 -5
  160. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +59 -18
  161. package/registry/tilt-card/files/src/lib/kit.tsx +8 -5
  162. package/registry/time-picker/files/src/components/ui/time-picker.tsx +156 -75
  163. package/registry/time-picker/files/src/lib/kit.tsx +8 -5
  164. package/registry/timeline/files/src/components/ui/avatar.tsx +7 -4
  165. package/registry/timeline/files/src/components/ui/timeline.tsx +19 -12
  166. package/registry/timeline/files/src/lib/kit.tsx +8 -5
  167. package/registry/toast/files/src/components/ui/button.tsx +35 -6
  168. package/registry/toast/files/src/components/ui/toast.tsx +25 -14
  169. package/registry/toast/files/src/lib/kit.tsx +8 -5
  170. package/schema/components.schema.json +1 -1
  171. package/registry/_docs/color-swatch.json +0 -82
  172. package/registry/_docs/kpi-card.json +0 -14
  173. package/registry/_docs/like-button.json +0 -92
  174. package/registry/_docs/morph-button.json +0 -128
  175. package/registry/_docs/pagination-dots.json +0 -108
  176. package/registry/_docs/shimmer-button.json +0 -98
  177. package/registry/_docs/tag-input.json +0 -148
  178. package/registry/color-swatch.json +0 -17
  179. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +0 -45
  180. package/registry/kpi-card.json +0 -14
  181. package/registry/like-button.json +0 -18
  182. package/registry/morph-button.json +0 -18
  183. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +0 -262
  184. package/registry/pagination-dots/files/src/lib/kit.tsx +0 -219
  185. package/registry/pagination-dots.json +0 -15
  186. package/registry/shimmer-button.json +0 -17
  187. package/registry/tag-input/files/src/components/ui/tag-input.tsx +0 -720
  188. package/registry/tag-input.json +0 -17
@@ -181,14 +181,17 @@ export function useKit(): Theme {
181
181
  return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
182
  }
183
183
 
184
- /** Shared motion presets so the whole kit moves the same way. */
184
+ /**
185
+ * Shared motion presets so the whole kit moves the same way. Every spring is critically damped
186
+ * (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
187
+ */
185
188
  export const motion = {
186
189
  /** Toggles, selection indicators. */
187
- snappy: { damping: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
190
+ snappy: { damping: 30, stiffness: 260, mass: 0.8 },
191
+ /** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
192
+ bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
190
193
  /** Sheets, layout changes. */
191
- gentle: { damping: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "fintech-app",
3
+ "type": "template",
4
+ "title": "Business banking app",
5
+ "description": "A complete Expo Router app for a small-business bank account: onboarding once, phone + OTP sign in, a live revenue dashboard, cards you can reveal and freeze, slide-to-pay transfers, profile and settings.",
6
+ "tier": "pro",
7
+ "dependencies": [
8
+ "react-native-svg",
9
+ "expo-haptics",
10
+ "expo-router",
11
+ "expo-splash-screen",
12
+ "expo-status-bar",
13
+ "react-native-safe-area-context",
14
+ "react-native-screens",
15
+ "@react-native-async-storage/async-storage"
16
+ ],
17
+ "registryDependencies": [
18
+ "kit",
19
+ "bottom-nav",
20
+ "onboarding",
21
+ "sign-in",
22
+ "dashboard",
23
+ "payment-card",
24
+ "switch",
25
+ "avatar",
26
+ "chip-group",
27
+ "slide-to-confirm",
28
+ "profile",
29
+ "settings"
30
+ ],
31
+ "sharedFiles": [
32
+ "lib/kit.tsx",
33
+ "components/ui/bottom-nav.tsx",
34
+ "components/blocks/onboarding.tsx",
35
+ "components/ui/button.tsx",
36
+ "components/ui/floating-input.tsx",
37
+ "components/ui/password-input.tsx",
38
+ "components/ui/otp-input.tsx",
39
+ "components/blocks/sign-in.tsx",
40
+ "components/ui/avatar.tsx",
41
+ "components/ui/badge.tsx",
42
+ "components/ui/list-item.tsx",
43
+ "components/ui/number-ticker.tsx",
44
+ "components/ui/segmented-control.tsx",
45
+ "components/ui/skeleton.tsx",
46
+ "components/blocks/dashboard.tsx",
47
+ "components/ui/tilt-card.tsx",
48
+ "components/ui/flip-card.tsx",
49
+ "components/ui/payment-card.tsx",
50
+ "components/ui/switch.tsx",
51
+ "components/ui/chip-group.tsx",
52
+ "components/ui/slide-to-confirm.tsx",
53
+ "components/blocks/profile.tsx",
54
+ "components/blocks/settings.tsx"
55
+ ],
56
+ "filesDir": null
57
+ }
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
- import { Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
2
+ import { AccessibilityInfo, Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
 
4
4
  import { motion } from '@/lib/kit';
5
5
 
@@ -48,12 +48,15 @@ const SQUEEZE = {
48
48
  * The flip as an animated 0 → 1 value, springing like FlipCard's own, plus `shadowScaleX` — pass
49
49
  * `progress` to the FlipCard and `shadowScaleX` to a TiltCard wrapped round it, so the tilt
50
50
  * shadow narrows as the card turns edge-on instead of showing as a dark box behind it.
51
+ * With Reduce Motion on, the card turns over at once (no 3D turn).
51
52
  */
52
53
  export function useFlipProgress(flipped: boolean) {
54
+ const reduce = useReduceMotion();
53
55
  const progress = useRef(new Animated.Value(flipped ? 1 : 0)).current;
54
56
  useEffect(() => {
55
- Animated.spring(progress, { toValue: flipped ? 1 : 0, ...motion.gentle, useNativeDriver: true }).start();
56
- }, [flipped, progress]);
57
+ if (reduce) progress.setValue(flipped ? 1 : 0);
58
+ else Animated.spring(progress, { toValue: flipped ? 1 : 0, ...motion.gentle, useNativeDriver: true }).start();
59
+ }, [flipped, progress, reduce]);
57
60
  const shadowScaleX = useMemo(() => progress.interpolate(SQUEEZE), [progress]);
58
61
  return { progress, shadowScaleX };
59
62
  }
@@ -121,7 +124,7 @@ export function FlipCard({
121
124
  borderRadius: radius,
122
125
  opacity,
123
126
  transform: [{ perspective }, { [axis]: turn } as never, { scale: lift }],
124
- }) as Animated.WithAnimatedObject<ViewStyle>;
127
+ }) as Animated.WithAnimatedValue<ViewStyle>;
125
128
 
126
129
  // The shadow squeezes along the turning axis as the card goes edge-on (see SQUEEZE) and fades a little.
127
130
  const squeeze = p.interpolate(SQUEEZE);
@@ -185,6 +188,22 @@ export function FlipCard({
185
188
  );
186
189
  }
187
190
 
191
+ function useReduceMotion() {
192
+ const [reduce, setReduce] = useState(false);
193
+ useEffect(() => {
194
+ let alive = true;
195
+ AccessibilityInfo.isReduceMotionEnabled()
196
+ .then((r) => alive && setReduce(r))
197
+ .catch(() => {});
198
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
199
+ return () => {
200
+ alive = false;
201
+ sub.remove();
202
+ };
203
+ }, []);
204
+ return reduce;
205
+ }
206
+
188
207
  const styles = StyleSheet.create({
189
208
  face: { overflow: 'hidden', backfaceVisibility: 'hidden' },
190
209
  back: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
@@ -181,14 +181,17 @@ export function useKit(): Theme {
181
181
  return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
182
  }
183
183
 
184
- /** Shared motion presets so the whole kit moves the same way. */
184
+ /**
185
+ * Shared motion presets so the whole kit moves the same way. Every spring is critically damped
186
+ * (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
187
+ */
185
188
  export const motion = {
186
189
  /** Toggles, selection indicators. */
187
- snappy: { damping: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
190
+ snappy: { damping: 30, stiffness: 260, mass: 0.8 },
191
+ /** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
192
+ bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
190
193
  /** Sheets, layout changes. */
191
- gentle: { damping: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };
@@ -1,5 +1,6 @@
1
- import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
1
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ComponentRef } from 'react';
2
2
  import {
3
+ AccessibilityInfo,
3
4
  Animated,
4
5
  Easing,
5
6
  Platform,
@@ -85,6 +86,9 @@ export const FLOATING_INPUT_SIZES = {
85
86
  md: { height: 54, font: 16, floated: 12.5, padX: 16, icon: 18, radius: 0 },
86
87
  lg: { height: 64, font: 18, floated: 13.5, padX: 18, icon: 20, radius: 4 },
87
88
  };
89
+ // Controls stop growing at iOS's largest standard text size (xxxL ≈ 1.35×) so they keep their shape;
90
+ // content text scales all the way.
91
+ const FONT_CAP = 1.35;
88
92
  const HALO = 4;
89
93
  /** Font size of the static label used when `floatingLabel` is false. */
90
94
  const LABEL_ABOVE_FONT = { sm: 13, md: 14, lg: 14.5 };
@@ -99,7 +103,7 @@ const NO_WEB_OUTLINE = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineW
99
103
  const AUTOFILL_CSS = `@keyframes ekAutofillOn{from{}to{}}@keyframes ekAutofillOff{from{}to{}}
100
104
  input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus{animation:ekAutofillOn 1ms;-webkit-text-fill-color:currentColor!important;caret-color:currentColor;transition:background-color 99999s 0s,color 99999s 0s!important}
101
105
  input:not(:-webkit-autofill){animation:ekAutofillOff 1ms}`;
102
- function useWebAutofill(inputRef: React.RefObject<TextInput | null>, onChange: (filled: boolean) => void) {
106
+ function useWebAutofill(inputRef: React.RefObject<ComponentRef<typeof TextInput> | null>, onChange: (filled: boolean) => void) {
103
107
  useEffect(() => {
104
108
  if (Platform.OS !== 'web' || typeof document === 'undefined') return;
105
109
  if (!document.getElementById('expokit-autofill')) {
@@ -126,7 +130,7 @@ function useWebAutofill(inputRef: React.RefObject<TextInput | null>, onChange: (
126
130
  * cutting a notch in the outline (Material-style). Focus draws a soft halo, errors shake,
127
131
  * success pops a check — all spring-driven.
128
132
  */
129
- export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function FloatingInput(
133
+ export const FloatingInput = forwardRef<ComponentRef<typeof TextInput>, FloatingInputProps>(function FloatingInput(
130
134
  {
131
135
  label,
132
136
  floatingLabel = true,
@@ -166,10 +170,11 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
166
170
  ref
167
171
  ) {
168
172
  const { colors, radius, font } = useKit();
173
+ const reduce = useReduceMotion();
169
174
  const ff = font.family ? { fontFamily: font.family } : null;
170
175
  const S = FLOATING_INPUT_SIZES[size];
171
176
  // The user's border colour only replaces the resting one; numeric height / radius drive the layout maths.
172
- const { borderColor: userBorder, ...userField } = StyleSheet.flatten(fieldStyle) ?? {};
177
+ const { borderColor: userBorder, ...userField } = flattenView(fieldStyle);
173
178
  const userLabelColor = StyleSheet.flatten(labelStyleProp)?.color;
174
179
  const HEIGHT = typeof userField.height === 'number' ? userField.height : S.height;
175
180
  // Floated size stays relative to the resting size, so a bigger label floats bigger at the same ratio.
@@ -179,8 +184,8 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
179
184
  const slot = S.icon + 10; // room for the clear button / check
180
185
  const tint = accent ?? colors.accent;
181
186
  const bg = surface ?? colors.background;
182
- const inputRef = useRef<TextInput>(null);
183
- useImperativeHandle(ref, () => inputRef.current as TextInput);
187
+ const inputRef = useRef<ComponentRef<typeof TextInput>>(null);
188
+ useImperativeHandle(ref, () => inputRef.current as ComponentRef<typeof TextInput>);
184
189
 
185
190
  const [focused, setFocused] = useState(false);
186
191
  const [inner, setInner] = useState(defaultValue ?? '');
@@ -200,29 +205,36 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
200
205
  const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
201
206
 
202
207
  useEffect(() => {
203
- Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
204
- }, [floating, float]);
208
+ if (reduce) float.setValue(floating ? 1 : 0);
209
+ else Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
210
+ }, [floating, float, reduce]);
205
211
 
206
212
  useEffect(() => {
207
213
  Animated.timing(focus, { toValue: isFocused ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
208
- Animated.spring(glow, { toValue: isFocused ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
209
- }, [isFocused, focus, glow]);
214
+ // Reduce Motion: halo and indicator bar only fade (their scales are pinned below).
215
+ if (reduce) Animated.timing(glow, { toValue: isFocused ? 1 : 0, duration: 150, useNativeDriver: true }).start();
216
+ else Animated.spring(glow, { toValue: isFocused ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
217
+ }, [isFocused, focus, glow, reduce]);
210
218
 
211
219
  useEffect(() => {
212
- Animated.spring(clear, { toValue: clearable && text && editable ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
213
- }, [clearable, text, editable, clear]);
220
+ const to = clearable && text && editable ? 1 : 0;
221
+ if (reduce) Animated.timing(clear, { toValue: to, duration: 150, useNativeDriver: true }).start();
222
+ else Animated.spring(clear, { toValue: to, useNativeDriver: true, ...motion.bouncy }).start();
223
+ }, [clearable, text, editable, clear, reduce]);
214
224
 
215
225
  useEffect(() => {
216
- Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
217
- }, [ok, check]);
226
+ if (reduce) Animated.timing(check, { toValue: ok ? 1 : 0, duration: 150, useNativeDriver: true }).start();
227
+ else Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
228
+ }, [ok, check, reduce]);
218
229
 
219
230
  useEffect(() => {
220
- if (!error) return;
231
+ // Reduce Motion: no shake; the red border and message still flag the error.
232
+ if (!error || reduce) return;
221
233
  shake.setValue(0);
222
234
  Animated.sequence(
223
235
  [10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))
224
236
  ).start();
225
- }, [error, shake]);
237
+ }, [error, shake, reduce]);
226
238
 
227
239
  const setText = (next: string) => {
228
240
  if (value === undefined) setInner(next);
@@ -310,23 +322,24 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
310
322
  borderRadius: corner + HALO,
311
323
  backgroundColor: active,
312
324
  opacity: glow.interpolate({ inputRange: [0, 1], outputRange: [0, 0.18], extrapolate: 'clamp' }),
313
- transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
325
+ transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.97, 1] }) }],
314
326
  },
315
327
  ]}
316
328
  />
317
329
  )}
318
330
  <Pressable onPress={() => inputRef.current?.focus()} disabled={!editable} accessible={false}>
319
- <Animated.View style={[styles.field, boxStyle, { height: HEIGHT }, userField, { borderColor }]}>
331
+ <Animated.View style={[styles.field, boxStyle, { minHeight: HEIGHT }, userField, { borderColor }]}>
320
332
  {left ? <View style={[styles.left, { left: underline ? 2 : S.padX - 2 }, underline && { top: contentTop }]}>{left}</View> : null}
321
333
  <View style={[styles.row, { paddingLeft: leftPad, paddingRight: rightPad, paddingTop: contentTop }]}>
322
334
  {prefix ? (
323
- <Animated.Text style={[styles.affix, styles.prefix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, prefixStyle, { opacity: affixOpacity }]}>
335
+ <Animated.Text maxFontSizeMultiplier={FONT_CAP} style={[styles.affix, styles.prefix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, prefixStyle, { opacity: affixOpacity }]}>
324
336
  {prefix}
325
337
  </Animated.Text>
326
338
  ) : null}
327
339
  <TextInput
328
340
  ref={inputRef}
329
341
  accessibilityLabel={label}
342
+ maxFontSizeMultiplier={FONT_CAP}
330
343
  {...props}
331
344
  maxLength={maxLength}
332
345
  editable={editable}
@@ -347,7 +360,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
347
360
  style={[styles.input, ff, { color: colors.foreground, fontSize: S.font }, inputStyle, NO_WEB_OUTLINE]}
348
361
  />
349
362
  {suffix ? (
350
- <Animated.Text style={[styles.affix, styles.suffix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, suffixStyle, { opacity: affixOpacity }]}>
363
+ <Animated.Text maxFontSizeMultiplier={FONT_CAP} style={[styles.affix, styles.suffix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, suffixStyle, { opacity: affixOpacity }]}>
351
364
  {suffix}
352
365
  </Animated.Text>
353
366
  ) : null}
@@ -358,6 +371,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
358
371
  style={[styles.labelWrap, { left: leftPad, right: right ? adorn : S.padX - 4, top: contentTop, height: HEIGHT - contentTop }, labelStyle]}
359
372
  >
360
373
  <Animated.Text
374
+ maxFontSizeMultiplier={FONT_CAP}
361
375
  numberOfLines={1}
362
376
  style={[
363
377
  styles.label,
@@ -380,7 +394,7 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
380
394
  ) : null}
381
395
  {clearable ? (
382
396
  <Animated.View
383
- style={[styles.slot, { right: edge, top: contentTop, opacity: clear, transform: [{ scale: clear }] }]}
397
+ style={[styles.slot, { right: edge, top: contentTop, opacity: clear, transform: [{ scale: reduce ? 1 : clear }] }]}
384
398
  pointerEvents={text && editable ? 'box-none' : 'none'}
385
399
  >
386
400
  <Pressable
@@ -408,8 +422,8 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
408
422
  top: contentTop,
409
423
  opacity: check.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
410
424
  transform: [
411
- { scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) },
412
- { rotate: check.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) },
425
+ { scale: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 1 : 0.3, 1] }) },
426
+ { rotate: check.interpolate({ inputRange: [0, 1], outputRange: [reduce ? '0deg' : '-30deg', '0deg'] }) },
413
427
  ],
414
428
  },
415
429
  ]}
@@ -423,10 +437,14 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
423
437
  {outlined ? null : (
424
438
  <>
425
439
  <View pointerEvents="none" style={[styles.baseLine, { height: underline ? 1.5 : 1, backgroundColor: rest }]} />
426
- {/* Grows out from the centre on focus. */}
440
+ {/* Grows out from the centre on focus (fades in with Reduce Motion). */}
427
441
  <Animated.View
428
442
  pointerEvents="none"
429
- style={[styles.bar, { backgroundColor: active, transform: [{ scaleX: glowClamped }] }]}
443
+ style={[
444
+ styles.bar,
445
+ { backgroundColor: active },
446
+ reduce ? { opacity: glowClamped } : { transform: [{ scaleX: glowClamped }] },
447
+ ]}
430
448
  />
431
449
  </>
432
450
  )}
@@ -436,10 +454,11 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
436
454
  {message || showCounter ? (
437
455
  <View style={[styles.footer, underline && styles.footerFlush]}>
438
456
  <View style={styles.messageSlot}>
439
- {message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} /> : null}
457
+ {message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} reduce={reduce} /> : null}
440
458
  </View>
441
459
  {showCounter ? (
442
460
  <Text
461
+ maxFontSizeMultiplier={FONT_CAP}
443
462
  style={[
444
463
  styles.count,
445
464
  ff,
@@ -459,13 +478,27 @@ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function
459
478
 
460
479
  type MessageTone = 'error' | 'success' | 'helper';
461
480
 
462
- /** Helper / error / success line. Error and success slide up and fade in. */
463
- function FieldMessage({ text, tone, color, style }: { text: string; tone: MessageTone; color: string; style?: StyleProp<TextStyle> }) {
481
+ /** Helper / error / success line. Error and success slide up and fade in (only fade with Reduce Motion). */
482
+ function FieldMessage({
483
+ text,
484
+ tone,
485
+ color,
486
+ style,
487
+ reduce,
488
+ }: {
489
+ text: string;
490
+ tone: MessageTone;
491
+ color: string;
492
+ style?: StyleProp<TextStyle>;
493
+ reduce: boolean;
494
+ }) {
464
495
  const { font } = useKit();
465
496
  const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
466
497
  useEffect(() => {
467
- if (tone !== 'helper') Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
468
- }, [tone, enter]);
498
+ if (tone === 'helper') return;
499
+ if (reduce) Animated.timing(enter, { toValue: 1, duration: 150, useNativeDriver: true }).start();
500
+ else Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
501
+ }, [tone, enter, reduce]);
469
502
 
470
503
  return (
471
504
  <Animated.View
@@ -473,7 +506,7 @@ function FieldMessage({ text, tone, color, style }: { text: string; tone: Messag
473
506
  styles.message,
474
507
  {
475
508
  opacity: enter.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
476
- transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }],
509
+ transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [reduce ? 0 : 4, 0] }) }],
477
510
  },
478
511
  ]}
479
512
  >
@@ -500,11 +533,37 @@ function FieldMessage({ text, tone, color, style }: { text: string; tone: Messag
500
533
  );
501
534
  }
502
535
 
536
+ /**
537
+ * The user's field style as one object. Not StyleSheet.flatten: with TypeScript 6 its RN 0.88 signature
538
+ * rejects a plain StyleProp<ViewStyle> once expo/types adds web-only keys to ViewStyle.
539
+ */
540
+ function flattenView(style: StyleProp<ViewStyle>): ViewStyle {
541
+ if (!style || typeof style !== 'object') return {};
542
+ if (Array.isArray(style)) return Object.assign({}, ...style.map((s) => flattenView(s as StyleProp<ViewStyle>)));
543
+ return style as ViewStyle;
544
+ }
545
+
546
+ function useReduceMotion() {
547
+ const [reduce, setReduce] = useState(false);
548
+ useEffect(() => {
549
+ let alive = true;
550
+ AccessibilityInfo.isReduceMotionEnabled()
551
+ .then((r) => alive && setReduce(r))
552
+ .catch(() => {});
553
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
554
+ return () => {
555
+ alive = false;
556
+ sub.remove();
557
+ };
558
+ }, []);
559
+ return reduce;
560
+ }
561
+
503
562
  const styles = StyleSheet.create({
504
563
  disabled: { opacity: 0.5 },
505
564
  halo: { position: 'absolute', top: -HALO, left: -HALO, right: -HALO, bottom: -HALO },
506
565
  field: { justifyContent: 'center' },
507
- row: { flexDirection: 'row', alignItems: 'center', height: '100%' },
566
+ row: { flexDirection: 'row', alignItems: 'center', flexGrow: 1 },
508
567
  input: { flex: 1, alignSelf: 'stretch', paddingVertical: 0, paddingHorizontal: 0 },
509
568
  affix: { fontWeight: '400' },
510
569
  prefix: { marginRight: 4 },
@@ -181,14 +181,17 @@ export function useKit(): Theme {
181
181
  return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
182
  }
183
183
 
184
- /** Shared motion presets so the whole kit moves the same way. */
184
+ /**
185
+ * Shared motion presets so the whole kit moves the same way. Every spring is critically damped
186
+ * (damping ≥ 2·√(stiffness·mass)): it settles smoothly with no overshoot or wobble.
187
+ */
185
188
  export const motion = {
186
189
  /** Toggles, selection indicators. */
187
- snappy: { damping: 18, stiffness: 260, mass: 0.8 },
188
- /** Press feedback, pops. */
189
- bouncy: { damping: 11, stiffness: 220, mass: 0.7 },
190
+ snappy: { damping: 30, stiffness: 260, mass: 0.8 },
191
+ /** Press feedback, pops. (Kept under its old name; it no longer bounces.) */
192
+ bouncy: { damping: 26, stiffness: 220, mass: 0.7 },
190
193
  /** Sheets, layout changes. */
191
- gentle: { damping: 22, stiffness: 160, mass: 1 },
194
+ gentle: { damping: 27, stiffness: 160, mass: 1 },
192
195
  fast: 160,
193
196
  normal: 240,
194
197
  };
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "food-app",
3
+ "type": "template",
4
+ "title": "Food ordering app",
5
+ "description": "A complete Expo Router app for a donut shop: onboarding once, sign in, a rolling menu, coupons, bag with checkout, profile and settings with a working light / dark switch.",
6
+ "tier": "pro",
7
+ "dependencies": [
8
+ "react-native-svg",
9
+ "expo-haptics",
10
+ "expo-router",
11
+ "expo-splash-screen",
12
+ "expo-status-bar",
13
+ "react-native-safe-area-context",
14
+ "react-native-screens",
15
+ "@react-native-async-storage/async-storage"
16
+ ],
17
+ "registryDependencies": [
18
+ "kit",
19
+ "bottom-nav",
20
+ "onboarding",
21
+ "sign-in",
22
+ "donut-shop",
23
+ "offers",
24
+ "checkout",
25
+ "profile",
26
+ "settings"
27
+ ],
28
+ "sharedFiles": [
29
+ "lib/kit.tsx",
30
+ "components/ui/bottom-nav.tsx",
31
+ "components/blocks/onboarding.tsx",
32
+ "components/ui/button.tsx",
33
+ "components/ui/floating-input.tsx",
34
+ "components/ui/password-input.tsx",
35
+ "components/ui/otp-input.tsx",
36
+ "components/blocks/sign-in.tsx",
37
+ "components/blocks/donut-shop.tsx",
38
+ "components/ui/tilt-card.tsx",
39
+ "components/ui/flip-card.tsx",
40
+ "lib/ticket-shape.tsx",
41
+ "components/ui/coupon.tsx",
42
+ "components/ui/confetti.tsx",
43
+ "components/blocks/offers.tsx",
44
+ "components/ui/slide-to-confirm.tsx",
45
+ "components/ui/stepper.tsx",
46
+ "components/blocks/checkout.tsx",
47
+ "components/blocks/profile.tsx",
48
+ "components/ui/avatar.tsx",
49
+ "components/ui/badge.tsx",
50
+ "components/ui/segmented-control.tsx",
51
+ "components/ui/switch.tsx",
52
+ "components/blocks/settings.tsx"
53
+ ],
54
+ "filesDir": null
55
+ }