@expokit/cli 0.0.0-stage → 0.2.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/bin/expo-kit.js +1204 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +338 -0
  6. package/package.json +44 -4
  7. package/registry/_docs/accordion.json +99 -0
  8. package/registry/_docs/action-sheet.json +110 -0
  9. package/registry/_docs/alert.json +127 -0
  10. package/registry/_docs/avatar.json +203 -0
  11. package/registry/_docs/badge.json +117 -0
  12. package/registry/_docs/bottom-nav.json +119 -0
  13. package/registry/_docs/bottom-sheet.json +158 -0
  14. package/registry/_docs/bullet.json +24 -0
  15. package/registry/_docs/button-3d.json +122 -0
  16. package/registry/_docs/button.json +115 -0
  17. package/registry/_docs/calendar.json +182 -0
  18. package/registry/_docs/card-stack.json +108 -0
  19. package/registry/_docs/card.json +111 -0
  20. package/registry/_docs/carousel.json +102 -0
  21. package/registry/_docs/checkbox.json +93 -0
  22. package/registry/_docs/checkout.json +102 -0
  23. package/registry/_docs/chip-group.json +116 -0
  24. package/registry/_docs/color-swatch.json +66 -0
  25. package/registry/_docs/confetti.json +82 -0
  26. package/registry/_docs/coupon.json +176 -0
  27. package/registry/_docs/cube-pager.json +84 -0
  28. package/registry/_docs/dashboard.json +120 -0
  29. package/registry/_docs/date-picker.json +249 -0
  30. package/registry/_docs/dialog.json +162 -0
  31. package/registry/_docs/drawer.json +171 -0
  32. package/registry/_docs/fab.json +120 -0
  33. package/registry/_docs/flip-card.json +111 -0
  34. package/registry/_docs/floating-input.json +191 -0
  35. package/registry/_docs/gift-card.json +108 -0
  36. package/registry/_docs/glass-button.json +118 -0
  37. package/registry/_docs/glass-nav-bar.json +78 -0
  38. package/registry/_docs/glass-search-bar.json +90 -0
  39. package/registry/_docs/glass-segmented.json +66 -0
  40. package/registry/_docs/glass-sheet.json +108 -0
  41. package/registry/_docs/glass-tab-bar.json +76 -0
  42. package/registry/_docs/glass.json +78 -0
  43. package/registry/_docs/inbox.json +72 -0
  44. package/registry/_docs/isometric-bar.json +72 -0
  45. package/registry/_docs/kit.json +11 -0
  46. package/registry/_docs/kpi-card.json +11 -0
  47. package/registry/_docs/like-button.json +72 -0
  48. package/registry/_docs/liquidglass-tab.json +27 -0
  49. package/registry/_docs/list-item.json +200 -0
  50. package/registry/_docs/loyalty-card.json +108 -0
  51. package/registry/_docs/morph-button.json +108 -0
  52. package/registry/_docs/number-ticker.json +96 -0
  53. package/registry/_docs/offers.json +78 -0
  54. package/registry/_docs/onboarding.json +66 -0
  55. package/registry/_docs/otp-input.json +134 -0
  56. package/registry/_docs/pagination-dots.json +88 -0
  57. package/registry/_docs/parallax-layers.json +84 -0
  58. package/registry/_docs/parallax-scroll.json +186 -0
  59. package/registry/_docs/password-input.json +92 -0
  60. package/registry/_docs/payment-card.json +153 -0
  61. package/registry/_docs/pixel-bar.json +66 -0
  62. package/registry/_docs/polar-area.json +78 -0
  63. package/registry/_docs/profile.json +84 -0
  64. package/registry/_docs/progress-steps.json +92 -0
  65. package/registry/_docs/progress.json +132 -0
  66. package/registry/_docs/radio-group.json +92 -0
  67. package/registry/_docs/rating.json +90 -0
  68. package/registry/_docs/ring-carousel.json +102 -0
  69. package/registry/_docs/scratch-card.json +90 -0
  70. package/registry/_docs/search-bar.json +140 -0
  71. package/registry/_docs/search-results.json +120 -0
  72. package/registry/_docs/segmented-control.json +103 -0
  73. package/registry/_docs/select.json +252 -0
  74. package/registry/_docs/settings.json +72 -0
  75. package/registry/_docs/shimmer-button.json +78 -0
  76. package/registry/_docs/sign-in.json +198 -0
  77. package/registry/_docs/skeleton.json +142 -0
  78. package/registry/_docs/slide-to-confirm.json +84 -0
  79. package/registry/_docs/slider.json +157 -0
  80. package/registry/_docs/spin-wheel.json +114 -0
  81. package/registry/_docs/spinner.json +91 -0
  82. package/registry/_docs/stepper.json +168 -0
  83. package/registry/_docs/swipe-deck.json +102 -0
  84. package/registry/_docs/swipeable-row.json +72 -0
  85. package/registry/_docs/switch.json +104 -0
  86. package/registry/_docs/tabs.json +105 -0
  87. package/registry/_docs/tag-input.json +128 -0
  88. package/registry/_docs/textarea.json +175 -0
  89. package/registry/_docs/ticket-card.json +174 -0
  90. package/registry/_docs/ticket-shape.json +11 -0
  91. package/registry/_docs/tilt-card.json +141 -0
  92. package/registry/_docs/time-picker.json +253 -0
  93. package/registry/_docs/timeline-chart.json +30 -0
  94. package/registry/_docs/timeline.json +156 -0
  95. package/registry/_docs/toast.json +161 -0
  96. package/registry/_docs/treemap.json +30 -0
  97. package/registry/_docs/waterfall.json +36 -0
  98. package/registry/_docs/wheel-list.json +102 -0
  99. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  100. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  101. package/registry/accordion.json +17 -0
  102. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  103. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  104. package/registry/action-sheet.json +18 -0
  105. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  106. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  107. package/registry/alert/files/src/lib/kit.tsx +219 -0
  108. package/registry/alert.json +20 -0
  109. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  110. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  111. package/registry/avatar.json +18 -0
  112. package/registry/badge/files/src/components/ui/badge.tsx +316 -0
  113. package/registry/badge/files/src/lib/kit.tsx +219 -0
  114. package/registry/badge.json +17 -0
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  117. package/registry/bottom-nav.json +18 -0
  118. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  119. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-sheet.json +17 -0
  121. package/registry/bullet.json +14 -0
  122. package/registry/button/files/src/components/ui/button.tsx +274 -0
  123. package/registry/button/files/src/lib/kit.tsx +219 -0
  124. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  125. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  126. package/registry/button-3d.json +18 -0
  127. package/registry/button.json +18 -0
  128. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  129. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  130. package/registry/calendar.json +17 -0
  131. package/registry/card/files/src/components/ui/card.tsx +300 -0
  132. package/registry/card/files/src/lib/kit.tsx +219 -0
  133. package/registry/card-stack.json +15 -0
  134. package/registry/card.json +17 -0
  135. package/registry/carousel.json +15 -0
  136. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  137. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  138. package/registry/checkbox.json +17 -0
  139. package/registry/checkout.json +26 -0
  140. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  141. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  142. package/registry/chip-group.json +17 -0
  143. package/registry/color-swatch.json +17 -0
  144. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  145. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  146. package/registry/confetti.json +15 -0
  147. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  148. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  149. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  150. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  151. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  152. package/registry/coupon.json +23 -0
  153. package/registry/cube-pager.json +15 -0
  154. package/registry/dashboard.json +32 -0
  155. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  156. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  157. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  158. package/registry/date-picker.json +19 -0
  159. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  160. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  161. package/registry/dialog.json +18 -0
  162. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  163. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  164. package/registry/drawer.json +18 -0
  165. package/registry/fab.json +18 -0
  166. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  167. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  168. package/registry/flip-card.json +15 -0
  169. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  170. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  171. package/registry/floating-input.json +17 -0
  172. package/registry/gift-card.json +21 -0
  173. package/registry/glass-button.json +22 -0
  174. package/registry/glass-nav-bar.json +21 -0
  175. package/registry/glass-search-bar.json +21 -0
  176. package/registry/glass-segmented.json +22 -0
  177. package/registry/glass-sheet.json +22 -0
  178. package/registry/glass-tab-bar.json +22 -0
  179. package/registry/glass.json +19 -0
  180. package/registry/inbox.json +30 -0
  181. package/registry/isometric-bar.json +14 -0
  182. package/registry/kit/files/src/lib/kit.tsx +219 -0
  183. package/registry/kit.json +11 -0
  184. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  185. package/registry/kpi-card.json +14 -0
  186. package/registry/like-button.json +18 -0
  187. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  188. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  189. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  190. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  192. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  193. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  194. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  195. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  196. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  197. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  198. package/registry/liquidglass-tab.json +23 -0
  199. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  200. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  201. package/registry/list-item.json +17 -0
  202. package/registry/loyalty-card.json +23 -0
  203. package/registry/morph-button.json +18 -0
  204. package/registry/number-ticker.json +15 -0
  205. package/registry/offers.json +24 -0
  206. package/registry/onboarding.json +17 -0
  207. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  208. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  209. package/registry/otp-input.json +17 -0
  210. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  211. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  212. package/registry/pagination-dots.json +15 -0
  213. package/registry/parallax-layers.json +15 -0
  214. package/registry/parallax-scroll.json +17 -0
  215. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  216. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  217. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  218. package/registry/password-input.json +19 -0
  219. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  220. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  221. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  222. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  223. package/registry/payment-card.json +21 -0
  224. package/registry/pixel-bar.json +14 -0
  225. package/registry/polar-area.json +14 -0
  226. package/registry/profile.json +17 -0
  227. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  228. package/registry/progress/files/src/lib/kit.tsx +219 -0
  229. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  230. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  231. package/registry/progress-steps.json +18 -0
  232. package/registry/progress.json +17 -0
  233. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  234. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  235. package/registry/radio-group.json +18 -0
  236. package/registry/rating.json +18 -0
  237. package/registry/ring-carousel.json +15 -0
  238. package/registry/scratch-card.json +18 -0
  239. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  240. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  241. package/registry/search-bar.json +17 -0
  242. package/registry/search-results.json +30 -0
  243. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  244. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  245. package/registry/segmented-control.json +18 -0
  246. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  247. package/registry/select/files/src/lib/kit.tsx +219 -0
  248. package/registry/select.json +17 -0
  249. package/registry/settings.json +26 -0
  250. package/registry/shimmer-button.json +17 -0
  251. package/registry/sign-in.json +26 -0
  252. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  253. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  254. package/registry/skeleton.json +17 -0
  255. package/registry/slide-to-confirm.json +18 -0
  256. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  257. package/registry/slider/files/src/lib/kit.tsx +219 -0
  258. package/registry/slider.json +17 -0
  259. package/registry/spin-wheel.json +18 -0
  260. package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
  261. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  262. package/registry/spinner.json +17 -0
  263. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  264. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  265. package/registry/stepper.json +17 -0
  266. package/registry/swipe-deck.json +15 -0
  267. package/registry/swipeable-row.json +15 -0
  268. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  269. package/registry/switch/files/src/lib/kit.tsx +219 -0
  270. package/registry/switch.json +18 -0
  271. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  272. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  273. package/registry/tabs.json +17 -0
  274. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  275. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  276. package/registry/tag-input.json +17 -0
  277. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  278. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  279. package/registry/textarea.json +17 -0
  280. package/registry/ticket-card.json +24 -0
  281. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  282. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  283. package/registry/ticket-shape.json +17 -0
  284. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  285. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  286. package/registry/tilt-card.json +17 -0
  287. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  288. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  289. package/registry/time-picker.json +17 -0
  290. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  291. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  292. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  293. package/registry/timeline-chart.json +14 -0
  294. package/registry/timeline.json +20 -0
  295. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  296. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  297. package/registry/toast/files/src/lib/kit.tsx +219 -0
  298. package/registry/toast.json +20 -0
  299. package/registry/treemap.json +14 -0
  300. package/registry/waterfall.json +14 -0
  301. package/registry/wheel-list.json +17 -0
  302. package/schema/components.schema.json +13 -0
@@ -0,0 +1,219 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { Platform, useColorScheme, type TextStyle } from 'react-native';
3
+
4
+ /**
5
+ * ExpoKit design tokens. Every component reads them through useKit(), so one
6
+ * <KitProvider> (optional) re-skins the whole kit: light / dark, accent colour, radius.
7
+ * Without a provider, components follow the system colour scheme.
8
+ */
9
+
10
+ /**
11
+ * Bumped whenever components start using a new kit API (2: radius.scale / radius.capsule).
12
+ * The CLI won't keep an edited kit.tsx older than the component being added needs.
13
+ */
14
+ export const KIT_VERSION = 2;
15
+
16
+ export type ColorScheme = 'light' | 'dark';
17
+
18
+ export interface Colors {
19
+ background: string;
20
+ foreground: string;
21
+ /** Cards, sheets, inputs. */
22
+ card: string;
23
+ cardForeground: string;
24
+ /** Raised surfaces: popovers, toasts. */
25
+ popover: string;
26
+ muted: string;
27
+ mutedForeground: string;
28
+ border: string;
29
+ input: string;
30
+ primary: string;
31
+ primaryForeground: string;
32
+ secondary: string;
33
+ secondaryForeground: string;
34
+ accent: string;
35
+ accentForeground: string;
36
+ success: string;
37
+ warning: string;
38
+ danger: string;
39
+ dangerForeground: string;
40
+ ring: string;
41
+ overlay: string;
42
+ }
43
+
44
+ export interface Theme {
45
+ dark: boolean;
46
+ colors: Colors;
47
+ /** Corner radius scale. `radius.full` makes pills. */
48
+ radius: {
49
+ sm: number;
50
+ md: number;
51
+ lg: number;
52
+ xl: number;
53
+ full: number;
54
+ /** A corner designed at the default radius (12), scaled to the current one: `radius.scale(16)` is 16 at 12, 0 at 0. */
55
+ scale: (n: number) => number;
56
+ /** Corner for a bar or chip of height `h`: a pill at radius ≥ 12, softer below, square at 0. */
57
+ capsule: (h: number) => number;
58
+ };
59
+ /** 4pt spacing scale: space(4) === 16. */
60
+ space: (n: number) => number;
61
+ font: { family?: string; mono: string };
62
+ text: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', TextStyle>;
63
+ }
64
+
65
+ const light: Colors = {
66
+ background: '#ffffff',
67
+ foreground: '#09090b',
68
+ card: '#ffffff',
69
+ cardForeground: '#09090b',
70
+ popover: '#ffffff',
71
+ muted: '#f4f4f5',
72
+ mutedForeground: '#71717a',
73
+ border: '#e4e4e7',
74
+ input: '#d4d4d8',
75
+ primary: '#18181b',
76
+ primaryForeground: '#fafafa',
77
+ secondary: '#f4f4f5',
78
+ secondaryForeground: '#18181b',
79
+ accent: '#18181b',
80
+ accentForeground: '#ffffff',
81
+ success: '#16a34a',
82
+ warning: '#f59e0b',
83
+ danger: '#e5484d',
84
+ dangerForeground: '#ffffff',
85
+ ring: '#18181b',
86
+ overlay: 'rgba(0,0,0,0.4)',
87
+ };
88
+
89
+ const dark: Colors = {
90
+ background: '#09090b',
91
+ foreground: '#fafafa',
92
+ card: '#111113',
93
+ cardForeground: '#fafafa',
94
+ popover: '#18181b',
95
+ muted: '#1c1c20',
96
+ mutedForeground: '#a1a1aa',
97
+ border: '#27272a',
98
+ input: '#34343a',
99
+ primary: '#fafafa',
100
+ primaryForeground: '#18181b',
101
+ secondary: '#1f1f23',
102
+ secondaryForeground: '#fafafa',
103
+ accent: '#fafafa',
104
+ accentForeground: '#09090b',
105
+ success: '#3dd68c',
106
+ warning: '#ffb224',
107
+ danger: '#ff6369',
108
+ dangerForeground: '#ffffff',
109
+ ring: '#d4d4d8',
110
+ overlay: 'rgba(0,0,0,0.6)',
111
+ };
112
+
113
+ export const palettes = { light, dark };
114
+
115
+ function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
116
+ const base = scheme === 'dark' ? dark : light;
117
+ const accent = overrides?.accent;
118
+ const colors: Colors = {
119
+ ...base,
120
+ ...(accent ? { accent, ring: accent, accentForeground: readableOn(accent) } : null),
121
+ ...overrides?.colors?.[scheme],
122
+ };
123
+ const r = overrides?.radius ?? 12;
124
+ return {
125
+ dark: scheme === 'dark',
126
+ colors,
127
+ radius: {
128
+ sm: Math.round(r * 0.5),
129
+ md: r,
130
+ lg: Math.round(r * 1.35),
131
+ xl: Math.round(r * 1.8),
132
+ full: 999,
133
+ scale: (n) => Math.round((n * r) / 12),
134
+ capsule: (h) => Math.round((h / 2) * Math.min(1, r / 12)),
135
+ },
136
+ space: (n) => n * 4,
137
+ font: {
138
+ family: overrides?.fontFamily,
139
+ mono: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'ui-monospace, SFMono-Regular, Menlo, monospace' })!,
140
+ },
141
+ text: {
142
+ xs: { fontSize: 12, lineHeight: 16 },
143
+ sm: { fontSize: 14, lineHeight: 20 },
144
+ md: { fontSize: 16, lineHeight: 22 },
145
+ lg: { fontSize: 18, lineHeight: 24, fontWeight: '400' },
146
+ xl: { fontSize: 22, lineHeight: 28, fontWeight: '400', letterSpacing: -0.3 },
147
+ xxl: { fontSize: 30, lineHeight: 36, fontWeight: '400', letterSpacing: -0.8 },
148
+ },
149
+ };
150
+ }
151
+
152
+ export interface ThemeOverrides {
153
+ /** Brand colour used for focus rings, selected states and accent buttons. */
154
+ accent?: string;
155
+ /** Base corner radius in points (default 12). */
156
+ radius?: number;
157
+ fontFamily?: string;
158
+ /** Override any token per scheme. */
159
+ colors?: Partial<Record<ColorScheme, Partial<Colors>>>;
160
+ }
161
+
162
+ const KitContext = createContext<{ scheme?: ColorScheme; overrides?: ThemeOverrides } | null>(null);
163
+
164
+ export function KitProvider({
165
+ scheme,
166
+ children,
167
+ ...overrides
168
+ }: ThemeOverrides & { scheme?: ColorScheme | 'system'; children: React.ReactNode }) {
169
+ const value = useMemo(
170
+ () => ({ scheme: scheme === 'system' ? undefined : scheme, overrides }),
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ [scheme, overrides.accent, overrides.radius, overrides.fontFamily, overrides.colors]
173
+ );
174
+ return <KitContext.Provider value={value}>{children}</KitContext.Provider>;
175
+ }
176
+
177
+ export function useKit(): Theme {
178
+ const ctx = useContext(KitContext);
179
+ const system = useColorScheme();
180
+ const scheme: ColorScheme = ctx?.scheme ?? (system === 'dark' ? 'dark' : 'light');
181
+ return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
+ }
183
+
184
+ /** Shared motion presets so the whole kit moves the same way. */
185
+ export const motion = {
186
+ /** 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
+ /** Sheets, layout changes. */
191
+ gentle: { damping: 22, stiffness: 160, mass: 1 },
192
+ fast: 160,
193
+ normal: 240,
194
+ };
195
+
196
+ /** Adds alpha to a #rrggbb colour: alpha('#6366f1', 0.2). */
197
+ export function alpha(hex: string, a: number) {
198
+ if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex;
199
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
200
+ return full + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0');
201
+ }
202
+
203
+ /** Black or white, whichever reads better on `hex`. */
204
+ export function readableOn(hex: string) {
205
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
206
+ const n = parseInt(full.slice(1, 7), 16);
207
+ if (Number.isNaN(n)) return '#ffffff';
208
+ const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
209
+ return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#09090b' : '#ffffff';
210
+ }
211
+
212
+ /**
213
+ * True for children that must be wrapped in <Text>: strings, numbers and arrays of them —
214
+ * `{count} new` is the array [3, ' new'], and raw text outside <Text> crashes on native.
215
+ */
216
+ export function isTextNode(node: React.ReactNode): node is string | number | (string | number)[] {
217
+ if (typeof node === 'string' || typeof node === 'number') return true;
218
+ return Array.isArray(node) && node.length > 0 && node.every((n) => typeof n === 'string' || typeof n === 'number');
219
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "drawer",
3
+ "type": "component",
4
+ "title": "Drawer",
5
+ "description": "Side navigation drawer that springs in over a fading scrim — or, with variant=\"push\", shrinks the screen into a rounded 0.92 card and slides it aside like modern mail apps. Swipe in from the edge, drag closed from anywhere on the panel with velocity and rubber-banding, and build the panel from DrawerProfile (soft initials avatar, presence, account switcher), DrawerItems with a spring-in accent-tinted active pill and popping count badges, divided section headings and a DrawerFooter meter. Flat theme colours, hairline edges and soft shadows; haptics throughout.",
6
+ "tier": "free",
7
+ "dependencies": [
8
+ "react-native-svg",
9
+ "expo-haptics"
10
+ ],
11
+ "registryDependencies": [
12
+ "kit"
13
+ ],
14
+ "sharedFiles": [
15
+ "lib/kit.tsx"
16
+ ],
17
+ "filesDir": "drawer/files"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "fab",
3
+ "type": "component",
4
+ "title": "Floating Action Button",
5
+ "description": "Corner FAB in a solid accent fill with a matching glow. Tapping it fans out a staggered speed dial — a labelled list or an arc — over a dimmed backdrop while the button turns neutral and the plus rotates into ×. Extended mode springs between a pill and a circle; an optional pulse ring draws the eye on empty states.",
6
+ "tier": "pro",
7
+ "dependencies": [
8
+ "react-native-svg",
9
+ "expo-haptics"
10
+ ],
11
+ "registryDependencies": [
12
+ "kit"
13
+ ],
14
+ "sharedFiles": [
15
+ "lib/kit.tsx"
16
+ ],
17
+ "filesDir": null
18
+ }
@@ -0,0 +1,205 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { Animated, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
3
+
4
+ import { motion } from '@/lib/kit';
5
+
6
+ export type FlipDirection = 'horizontal' | 'vertical';
7
+ export type FlipTrigger = 'press' | 'none';
8
+
9
+ export interface FlipCardProps {
10
+ /** Shown first. Sets the card's size — the back fills the same box. */
11
+ front: React.ReactNode;
12
+ back: React.ReactNode;
13
+ /** Controlled side: `true` shows the back. */
14
+ flipped?: boolean;
15
+ /** Uncontrolled initial side (default false). */
16
+ defaultFlipped?: boolean;
17
+ onFlip?: (flipped: boolean) => void;
18
+ /** Spin around the vertical axis (left/right) or the horizontal one (top/bottom). */
19
+ direction?: FlipDirection;
20
+ /** `press` flips on tap; `none` leaves it to `flipped` (e.g. a button elsewhere). */
21
+ trigger?: FlipTrigger;
22
+ /** Depth of the 3D effect — smaller is more dramatic (default 1000). */
23
+ perspective?: number;
24
+ /** Corner radius of both faces (default 20). */
25
+ radius?: number;
26
+ /** Soft shadow that narrows as the card turns edge-on (default true). */
27
+ shadow?: boolean;
28
+ /**
29
+ * Drive the turn from outside (0 = front, 1 = back) — from `useFlipProgress`, when something else
30
+ * must follow the flip, like a TiltCard's shadow. `flipped` still sets the side for accessibility.
31
+ */
32
+ progress?: Animated.Value;
33
+ /** Accessibility label; the hint says which side is showing. */
34
+ accessibilityLabel?: string;
35
+ /** Screen-reader hints (defaults in English). */
36
+ labels?: { toBack?: string; toFront?: string };
37
+ style?: StyleProp<ViewStyle>;
38
+ }
39
+
40
+ // How wide the shadow under a flipping card is through the turn: roughly cos θ, always a little
41
+ // narrower than the card so a flat shadow plate never shows past it, and none at all edge-on.
42
+ const SQUEEZE = {
43
+ inputRange: [0, 0.25, 0.45, 0.5, 0.55, 0.75, 1],
44
+ outputRange: [1, 0.6, 0.12, 0, 0.12, 0.6, 1],
45
+ };
46
+
47
+ /**
48
+ * The flip as an animated 0 → 1 value, springing like FlipCard's own, plus `shadowScaleX` — pass
49
+ * `progress` to the FlipCard and `shadowScaleX` to a TiltCard wrapped round it, so the tilt
50
+ * shadow narrows as the card turns edge-on instead of showing as a dark box behind it.
51
+ */
52
+ export function useFlipProgress(flipped: boolean) {
53
+ const progress = useRef(new Animated.Value(flipped ? 1 : 0)).current;
54
+ useEffect(() => {
55
+ Animated.spring(progress, { toValue: flipped ? 1 : 0, ...motion.gentle, useNativeDriver: true }).start();
56
+ }, [flipped, progress]);
57
+ const shadowScaleX = useMemo(() => progress.interpolate(SQUEEZE), [progress]);
58
+ return { progress, shadowScaleX };
59
+ }
60
+
61
+ /**
62
+ * A card with two faces that flips in 3D on tap — it lifts toward you half-way through and its
63
+ * shadow narrows as it turns edge-on, like a real card turning over. Controlled or uncontrolled.
64
+ */
65
+ export function FlipCard({
66
+ front,
67
+ back,
68
+ flipped: flippedProp,
69
+ defaultFlipped = false,
70
+ onFlip,
71
+ direction = 'horizontal',
72
+ trigger = 'press',
73
+ perspective = 1000,
74
+ radius = 20,
75
+ shadow = true,
76
+ progress,
77
+ accessibilityLabel,
78
+ labels,
79
+ style,
80
+ }: FlipCardProps) {
81
+ const controlled = flippedProp !== undefined;
82
+ const [own, setOwn] = useState(defaultFlipped);
83
+ const flipped = controlled ? flippedProp : own;
84
+
85
+ const turn = useFlipProgress(flipped).progress;
86
+ const p = progress ?? turn;
87
+
88
+ const toggle = () => {
89
+ const next = !flipped;
90
+ if (!controlled) setOwn(next);
91
+ onFlip?.(next);
92
+ };
93
+
94
+ const horizontal = direction === 'horizontal';
95
+ const axis = horizontal ? 'rotateY' : 'rotateX';
96
+ const frontTurn = p.interpolate({
97
+ inputRange: [0, 1],
98
+ outputRange: ['0deg', '180deg'],
99
+ });
100
+ const backTurn = p.interpolate({
101
+ inputRange: [0, 1],
102
+ outputRange: ['180deg', '360deg'],
103
+ });
104
+ // Lift toward the viewer half-way through the turn.
105
+ const lift = p.interpolate({
106
+ inputRange: [0, 0.5, 1],
107
+ outputRange: [1, 1.05, 1],
108
+ });
109
+ // Swap faces exactly at 90° — backfaceVisibility alone flickers on some Android versions.
110
+ const frontOpacity = p.interpolate({
111
+ inputRange: [0, 0.499, 0.5, 1],
112
+ outputRange: [1, 1, 0, 0],
113
+ });
114
+ const backOpacity = p.interpolate({
115
+ inputRange: [0, 0.499, 0.5, 1],
116
+ outputRange: [0, 0, 1, 1],
117
+ });
118
+
119
+ const face = (turn: Animated.AnimatedInterpolation<string>, opacity: Animated.AnimatedInterpolation<number>) =>
120
+ ({
121
+ borderRadius: radius,
122
+ opacity,
123
+ transform: [{ perspective }, { [axis]: turn } as never, { scale: lift }],
124
+ }) as Animated.WithAnimatedObject<ViewStyle>;
125
+
126
+ // The shadow squeezes along the turning axis as the card goes edge-on (see SQUEEZE) and fades a little.
127
+ const squeeze = p.interpolate(SQUEEZE);
128
+
129
+ const body = (
130
+ <View>
131
+ {shadow ? (
132
+ <Animated.View
133
+ pointerEvents="none"
134
+ style={[
135
+ styles.plate,
136
+ {
137
+ borderRadius: radius,
138
+ opacity: p.interpolate({
139
+ inputRange: [0, 0.5, 1],
140
+ outputRange: [1, 0.4, 1],
141
+ }),
142
+ transform: [{ scale: 0.9 }, horizontal ? { scaleX: squeeze } : { scaleY: squeeze }],
143
+ },
144
+ ]}
145
+ />
146
+ ) : null}
147
+ {/* Faces live in their own (non-collapsing) layer: iOS intersects sibling layers in 3D, so a
148
+ turning face next to the flat shadow plate would be sliced by it. */}
149
+ {/* On Android the wrapper carries the elevation that keeps the faces above the plate; the faces
150
+ get none, since a face turned in 3D would cast a warped shadow wedge below the card. */}
151
+ <View collapsable={false} style={{ elevation: shadow ? 23 : 0 }}>
152
+ {/* The hidden face stays mounted (for the turn) but is hidden from screen readers. */}
153
+ <Animated.View
154
+ style={[styles.face, face(frontTurn, frontOpacity)]}
155
+ pointerEvents={flipped ? 'none' : 'auto'}
156
+ accessibilityElementsHidden={flipped}
157
+ importantForAccessibility={flipped ? 'no-hide-descendants' : 'auto'}
158
+ >
159
+ {front}
160
+ </Animated.View>
161
+ <Animated.View
162
+ style={[styles.face, styles.back, face(backTurn, backOpacity)]}
163
+ pointerEvents={flipped ? 'auto' : 'none'}
164
+ accessibilityElementsHidden={!flipped}
165
+ importantForAccessibility={flipped ? 'auto' : 'no-hide-descendants'}
166
+ >
167
+ {back}
168
+ </Animated.View>
169
+ </View>
170
+ </View>
171
+ );
172
+
173
+ if (trigger === 'none') return <View style={style}>{body}</View>;
174
+ return (
175
+ <Pressable
176
+ onPress={toggle}
177
+ accessibilityRole="button"
178
+ accessibilityLabel={accessibilityLabel}
179
+ accessibilityHint={flipped ? (labels?.toFront ?? 'Showing the back. Tap to flip to the front.') : (labels?.toBack ?? 'Tap to flip to the back.')}
180
+ accessibilityState={{ expanded: flipped }}
181
+ style={style}
182
+ >
183
+ {body}
184
+ </Pressable>
185
+ );
186
+ }
187
+
188
+ const styles = StyleSheet.create({
189
+ face: { overflow: 'hidden', backfaceVisibility: 'hidden' },
190
+ back: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 },
191
+ plate: {
192
+ position: 'absolute',
193
+ top: 0,
194
+ left: 0,
195
+ right: 0,
196
+ bottom: 0,
197
+ backgroundColor: '#000',
198
+ shadowColor: '#000',
199
+ shadowOpacity: 0.4,
200
+ shadowRadius: 20,
201
+ shadowOffset: { width: 0, height: 14 },
202
+ // High enough that Android's shadow spreads past the inset plate (iOS uses the shadow* props).
203
+ elevation: 22,
204
+ },
205
+ });
@@ -0,0 +1,219 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { Platform, useColorScheme, type TextStyle } from 'react-native';
3
+
4
+ /**
5
+ * ExpoKit design tokens. Every component reads them through useKit(), so one
6
+ * <KitProvider> (optional) re-skins the whole kit: light / dark, accent colour, radius.
7
+ * Without a provider, components follow the system colour scheme.
8
+ */
9
+
10
+ /**
11
+ * Bumped whenever components start using a new kit API (2: radius.scale / radius.capsule).
12
+ * The CLI won't keep an edited kit.tsx older than the component being added needs.
13
+ */
14
+ export const KIT_VERSION = 2;
15
+
16
+ export type ColorScheme = 'light' | 'dark';
17
+
18
+ export interface Colors {
19
+ background: string;
20
+ foreground: string;
21
+ /** Cards, sheets, inputs. */
22
+ card: string;
23
+ cardForeground: string;
24
+ /** Raised surfaces: popovers, toasts. */
25
+ popover: string;
26
+ muted: string;
27
+ mutedForeground: string;
28
+ border: string;
29
+ input: string;
30
+ primary: string;
31
+ primaryForeground: string;
32
+ secondary: string;
33
+ secondaryForeground: string;
34
+ accent: string;
35
+ accentForeground: string;
36
+ success: string;
37
+ warning: string;
38
+ danger: string;
39
+ dangerForeground: string;
40
+ ring: string;
41
+ overlay: string;
42
+ }
43
+
44
+ export interface Theme {
45
+ dark: boolean;
46
+ colors: Colors;
47
+ /** Corner radius scale. `radius.full` makes pills. */
48
+ radius: {
49
+ sm: number;
50
+ md: number;
51
+ lg: number;
52
+ xl: number;
53
+ full: number;
54
+ /** A corner designed at the default radius (12), scaled to the current one: `radius.scale(16)` is 16 at 12, 0 at 0. */
55
+ scale: (n: number) => number;
56
+ /** Corner for a bar or chip of height `h`: a pill at radius ≥ 12, softer below, square at 0. */
57
+ capsule: (h: number) => number;
58
+ };
59
+ /** 4pt spacing scale: space(4) === 16. */
60
+ space: (n: number) => number;
61
+ font: { family?: string; mono: string };
62
+ text: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', TextStyle>;
63
+ }
64
+
65
+ const light: Colors = {
66
+ background: '#ffffff',
67
+ foreground: '#09090b',
68
+ card: '#ffffff',
69
+ cardForeground: '#09090b',
70
+ popover: '#ffffff',
71
+ muted: '#f4f4f5',
72
+ mutedForeground: '#71717a',
73
+ border: '#e4e4e7',
74
+ input: '#d4d4d8',
75
+ primary: '#18181b',
76
+ primaryForeground: '#fafafa',
77
+ secondary: '#f4f4f5',
78
+ secondaryForeground: '#18181b',
79
+ accent: '#18181b',
80
+ accentForeground: '#ffffff',
81
+ success: '#16a34a',
82
+ warning: '#f59e0b',
83
+ danger: '#e5484d',
84
+ dangerForeground: '#ffffff',
85
+ ring: '#18181b',
86
+ overlay: 'rgba(0,0,0,0.4)',
87
+ };
88
+
89
+ const dark: Colors = {
90
+ background: '#09090b',
91
+ foreground: '#fafafa',
92
+ card: '#111113',
93
+ cardForeground: '#fafafa',
94
+ popover: '#18181b',
95
+ muted: '#1c1c20',
96
+ mutedForeground: '#a1a1aa',
97
+ border: '#27272a',
98
+ input: '#34343a',
99
+ primary: '#fafafa',
100
+ primaryForeground: '#18181b',
101
+ secondary: '#1f1f23',
102
+ secondaryForeground: '#fafafa',
103
+ accent: '#fafafa',
104
+ accentForeground: '#09090b',
105
+ success: '#3dd68c',
106
+ warning: '#ffb224',
107
+ danger: '#ff6369',
108
+ dangerForeground: '#ffffff',
109
+ ring: '#d4d4d8',
110
+ overlay: 'rgba(0,0,0,0.6)',
111
+ };
112
+
113
+ export const palettes = { light, dark };
114
+
115
+ function build(scheme: ColorScheme, overrides?: ThemeOverrides): Theme {
116
+ const base = scheme === 'dark' ? dark : light;
117
+ const accent = overrides?.accent;
118
+ const colors: Colors = {
119
+ ...base,
120
+ ...(accent ? { accent, ring: accent, accentForeground: readableOn(accent) } : null),
121
+ ...overrides?.colors?.[scheme],
122
+ };
123
+ const r = overrides?.radius ?? 12;
124
+ return {
125
+ dark: scheme === 'dark',
126
+ colors,
127
+ radius: {
128
+ sm: Math.round(r * 0.5),
129
+ md: r,
130
+ lg: Math.round(r * 1.35),
131
+ xl: Math.round(r * 1.8),
132
+ full: 999,
133
+ scale: (n) => Math.round((n * r) / 12),
134
+ capsule: (h) => Math.round((h / 2) * Math.min(1, r / 12)),
135
+ },
136
+ space: (n) => n * 4,
137
+ font: {
138
+ family: overrides?.fontFamily,
139
+ mono: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'ui-monospace, SFMono-Regular, Menlo, monospace' })!,
140
+ },
141
+ text: {
142
+ xs: { fontSize: 12, lineHeight: 16 },
143
+ sm: { fontSize: 14, lineHeight: 20 },
144
+ md: { fontSize: 16, lineHeight: 22 },
145
+ lg: { fontSize: 18, lineHeight: 24, fontWeight: '400' },
146
+ xl: { fontSize: 22, lineHeight: 28, fontWeight: '400', letterSpacing: -0.3 },
147
+ xxl: { fontSize: 30, lineHeight: 36, fontWeight: '400', letterSpacing: -0.8 },
148
+ },
149
+ };
150
+ }
151
+
152
+ export interface ThemeOverrides {
153
+ /** Brand colour used for focus rings, selected states and accent buttons. */
154
+ accent?: string;
155
+ /** Base corner radius in points (default 12). */
156
+ radius?: number;
157
+ fontFamily?: string;
158
+ /** Override any token per scheme. */
159
+ colors?: Partial<Record<ColorScheme, Partial<Colors>>>;
160
+ }
161
+
162
+ const KitContext = createContext<{ scheme?: ColorScheme; overrides?: ThemeOverrides } | null>(null);
163
+
164
+ export function KitProvider({
165
+ scheme,
166
+ children,
167
+ ...overrides
168
+ }: ThemeOverrides & { scheme?: ColorScheme | 'system'; children: React.ReactNode }) {
169
+ const value = useMemo(
170
+ () => ({ scheme: scheme === 'system' ? undefined : scheme, overrides }),
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ [scheme, overrides.accent, overrides.radius, overrides.fontFamily, overrides.colors]
173
+ );
174
+ return <KitContext.Provider value={value}>{children}</KitContext.Provider>;
175
+ }
176
+
177
+ export function useKit(): Theme {
178
+ const ctx = useContext(KitContext);
179
+ const system = useColorScheme();
180
+ const scheme: ColorScheme = ctx?.scheme ?? (system === 'dark' ? 'dark' : 'light');
181
+ return useMemo(() => build(scheme, ctx?.overrides), [scheme, ctx?.overrides]);
182
+ }
183
+
184
+ /** Shared motion presets so the whole kit moves the same way. */
185
+ export const motion = {
186
+ /** 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
+ /** Sheets, layout changes. */
191
+ gentle: { damping: 22, stiffness: 160, mass: 1 },
192
+ fast: 160,
193
+ normal: 240,
194
+ };
195
+
196
+ /** Adds alpha to a #rrggbb colour: alpha('#6366f1', 0.2). */
197
+ export function alpha(hex: string, a: number) {
198
+ if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex;
199
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
200
+ return full + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0');
201
+ }
202
+
203
+ /** Black or white, whichever reads better on `hex`. */
204
+ export function readableOn(hex: string) {
205
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
206
+ const n = parseInt(full.slice(1, 7), 16);
207
+ if (Number.isNaN(n)) return '#ffffff';
208
+ const [r, g, b] = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
209
+ return (r * 299 + g * 587 + b * 114) / 1000 > 150 ? '#09090b' : '#ffffff';
210
+ }
211
+
212
+ /**
213
+ * True for children that must be wrapped in <Text>: strings, numbers and arrays of them —
214
+ * `{count} new` is the array [3, ' new'], and raw text outside <Text> crashes on native.
215
+ */
216
+ export function isTextNode(node: React.ReactNode): node is string | number | (string | number)[] {
217
+ if (typeof node === 'string' || typeof node === 'number') return true;
218
+ return Array.isArray(node) && node.length > 0 && node.every((n) => typeof n === 'string' || typeof n === 'number');
219
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "flip-card",
3
+ "type": "component",
4
+ "title": "Flip Card",
5
+ "description": "Two-sided card that turns over in 3D on tap — it lifts toward you half-way and its shadow narrows as it goes edge-on, like a real card. Flips left/right or top/bottom, controlled or uncontrolled, and swaps faces exactly at 90° so Android never flickers.",
6
+ "tier": "free",
7
+ "dependencies": [],
8
+ "registryDependencies": [
9
+ "kit"
10
+ ],
11
+ "sharedFiles": [
12
+ "lib/kit.tsx"
13
+ ],
14
+ "filesDir": "flip-card/files"
15
+ }