@expokit/cli 0.0.0-stage → 0.3.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 (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/bin/expo-kit.js +1266 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +345 -0
  6. package/package.json +49 -4
  7. package/registry/_docs/accordion.json +115 -0
  8. package/registry/_docs/action-sheet.json +138 -0
  9. package/registry/_docs/alert.json +155 -0
  10. package/registry/_docs/avatar.json +227 -0
  11. package/registry/_docs/badge.json +148 -0
  12. package/registry/_docs/bottom-nav.json +143 -0
  13. package/registry/_docs/bottom-sheet.json +186 -0
  14. package/registry/_docs/bullet.json +27 -0
  15. package/registry/_docs/button-3d.json +138 -0
  16. package/registry/_docs/button.json +131 -0
  17. package/registry/_docs/calendar.json +198 -0
  18. package/registry/_docs/card-stack.json +124 -0
  19. package/registry/_docs/card.json +131 -0
  20. package/registry/_docs/carousel.json +122 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +109 -0
  23. package/registry/_docs/checkout.json +105 -0
  24. package/registry/_docs/chip-group.json +132 -0
  25. package/registry/_docs/color-swatch.json +82 -0
  26. package/registry/_docs/confetti.json +102 -0
  27. package/registry/_docs/coupon.json +192 -0
  28. package/registry/_docs/cube-pager.json +100 -0
  29. package/registry/_docs/dashboard.json +123 -0
  30. package/registry/_docs/date-picker.json +269 -0
  31. package/registry/_docs/dialog.json +186 -0
  32. package/registry/_docs/drawer.json +199 -0
  33. package/registry/_docs/fab.json +144 -0
  34. package/registry/_docs/flip-card.json +127 -0
  35. package/registry/_docs/floating-input.json +211 -0
  36. package/registry/_docs/gift-card.json +124 -0
  37. package/registry/_docs/glass-button.json +142 -0
  38. package/registry/_docs/glass-nav-bar.json +102 -0
  39. package/registry/_docs/glass-search-bar.json +114 -0
  40. package/registry/_docs/glass-segmented.json +94 -0
  41. package/registry/_docs/glass-sheet.json +136 -0
  42. package/registry/_docs/glass-tab-bar.json +100 -0
  43. package/registry/_docs/glass.json +94 -0
  44. package/registry/_docs/inbox.json +75 -0
  45. package/registry/_docs/isometric-bar.json +75 -0
  46. package/registry/_docs/kit.json +14 -0
  47. package/registry/_docs/kpi-card.json +14 -0
  48. package/registry/_docs/like-button.json +92 -0
  49. package/registry/_docs/liquidglass-tab.json +30 -0
  50. package/registry/_docs/list-item.json +216 -0
  51. package/registry/_docs/loyalty-card.json +124 -0
  52. package/registry/_docs/morph-button.json +128 -0
  53. package/registry/_docs/number-ticker.json +112 -0
  54. package/registry/_docs/offers.json +81 -0
  55. package/registry/_docs/onboarding.json +69 -0
  56. package/registry/_docs/otp-input.json +158 -0
  57. package/registry/_docs/pagination-dots.json +108 -0
  58. package/registry/_docs/parallax-layers.json +104 -0
  59. package/registry/_docs/parallax-scroll.json +210 -0
  60. package/registry/_docs/password-input.json +112 -0
  61. package/registry/_docs/payment-card.json +169 -0
  62. package/registry/_docs/pixel-bar.json +69 -0
  63. package/registry/_docs/polar-area.json +81 -0
  64. package/registry/_docs/profile.json +87 -0
  65. package/registry/_docs/progress-steps.json +116 -0
  66. package/registry/_docs/progress.json +152 -0
  67. package/registry/_docs/radio-group.json +112 -0
  68. package/registry/_docs/rating.json +114 -0
  69. package/registry/_docs/ring-carousel.json +122 -0
  70. package/registry/_docs/scratch-card.json +110 -0
  71. package/registry/_docs/search-bar.json +160 -0
  72. package/registry/_docs/search-results.json +123 -0
  73. package/registry/_docs/segmented-control.json +123 -0
  74. package/registry/_docs/select.json +277 -0
  75. package/registry/_docs/settings.json +75 -0
  76. package/registry/_docs/shimmer-button.json +98 -0
  77. package/registry/_docs/sign-in.json +201 -0
  78. package/registry/_docs/skeleton.json +162 -0
  79. package/registry/_docs/slide-to-confirm.json +108 -0
  80. package/registry/_docs/slider.json +177 -0
  81. package/registry/_docs/spin-wheel.json +138 -0
  82. package/registry/_docs/spinner.json +121 -0
  83. package/registry/_docs/stateful-button.json +184 -0
  84. package/registry/_docs/stepper.json +184 -0
  85. package/registry/_docs/swipe-deck.json +118 -0
  86. package/registry/_docs/swipeable-row.json +95 -0
  87. package/registry/_docs/switch.json +124 -0
  88. package/registry/_docs/tabs.json +129 -0
  89. package/registry/_docs/tag-input.json +148 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +191 -0
  92. package/registry/_docs/ticket-card.json +194 -0
  93. package/registry/_docs/ticket-shape.json +14 -0
  94. package/registry/_docs/tilt-card.json +157 -0
  95. package/registry/_docs/time-picker.json +269 -0
  96. package/registry/_docs/timeline-chart.json +33 -0
  97. package/registry/_docs/timeline.json +172 -0
  98. package/registry/_docs/toast.json +185 -0
  99. package/registry/_docs/treemap.json +33 -0
  100. package/registry/_docs/waterfall.json +39 -0
  101. package/registry/_docs/wheel-list.json +122 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  103. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  104. package/registry/accordion.json +17 -0
  105. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  106. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  107. package/registry/action-sheet.json +18 -0
  108. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  109. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  110. package/registry/alert/files/src/lib/kit.tsx +219 -0
  111. package/registry/alert.json +20 -0
  112. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  113. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  114. package/registry/avatar.json +18 -0
  115. package/registry/badge/files/src/components/ui/badge.tsx +626 -0
  116. package/registry/badge/files/src/lib/kit.tsx +219 -0
  117. package/registry/badge.json +17 -0
  118. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  119. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-nav.json +18 -0
  121. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  122. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  123. package/registry/bottom-sheet.json +17 -0
  124. package/registry/bullet.json +14 -0
  125. package/registry/button/files/src/components/ui/button.tsx +274 -0
  126. package/registry/button/files/src/lib/kit.tsx +219 -0
  127. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  128. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  129. package/registry/button-3d.json +18 -0
  130. package/registry/button.json +18 -0
  131. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  132. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  133. package/registry/calendar.json +17 -0
  134. package/registry/card/files/src/components/ui/card.tsx +300 -0
  135. package/registry/card/files/src/lib/kit.tsx +219 -0
  136. package/registry/card-stack.json +15 -0
  137. package/registry/card.json +17 -0
  138. package/registry/carousel.json +15 -0
  139. package/registry/chat.json +18 -0
  140. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  141. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  142. package/registry/checkbox.json +17 -0
  143. package/registry/checkout.json +26 -0
  144. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  145. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  146. package/registry/chip-group.json +17 -0
  147. package/registry/color-swatch.json +17 -0
  148. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  149. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  150. package/registry/confetti.json +15 -0
  151. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  152. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  153. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  154. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  155. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  156. package/registry/coupon.json +23 -0
  157. package/registry/cube-pager.json +15 -0
  158. package/registry/dashboard.json +32 -0
  159. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  160. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  161. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  162. package/registry/date-picker.json +19 -0
  163. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  164. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  165. package/registry/dialog.json +18 -0
  166. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  167. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  168. package/registry/drawer.json +18 -0
  169. package/registry/fab.json +18 -0
  170. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  171. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  172. package/registry/flip-card.json +15 -0
  173. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  174. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  175. package/registry/floating-input.json +17 -0
  176. package/registry/gift-card.json +21 -0
  177. package/registry/glass-button.json +22 -0
  178. package/registry/glass-nav-bar.json +21 -0
  179. package/registry/glass-search-bar.json +21 -0
  180. package/registry/glass-segmented.json +22 -0
  181. package/registry/glass-sheet.json +22 -0
  182. package/registry/glass-tab-bar.json +22 -0
  183. package/registry/glass.json +19 -0
  184. package/registry/inbox.json +30 -0
  185. package/registry/isometric-bar.json +14 -0
  186. package/registry/kit/files/src/lib/kit.tsx +219 -0
  187. package/registry/kit.json +11 -0
  188. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  189. package/registry/kpi-card.json +14 -0
  190. package/registry/like-button.json +18 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  192. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  193. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  194. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  195. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  196. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  197. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  198. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  199. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  200. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  201. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  202. package/registry/liquidglass-tab.json +23 -0
  203. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  204. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  205. package/registry/list-item.json +17 -0
  206. package/registry/loyalty-card.json +23 -0
  207. package/registry/morph-button.json +18 -0
  208. package/registry/number-ticker.json +15 -0
  209. package/registry/offers.json +24 -0
  210. package/registry/onboarding.json +17 -0
  211. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  212. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  213. package/registry/otp-input.json +17 -0
  214. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  215. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  216. package/registry/pagination-dots.json +15 -0
  217. package/registry/parallax-layers.json +15 -0
  218. package/registry/parallax-scroll.json +17 -0
  219. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  220. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  221. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  222. package/registry/password-input.json +19 -0
  223. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  224. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  225. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  226. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  227. package/registry/payment-card.json +21 -0
  228. package/registry/pixel-bar.json +14 -0
  229. package/registry/polar-area.json +14 -0
  230. package/registry/profile.json +17 -0
  231. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  232. package/registry/progress/files/src/lib/kit.tsx +219 -0
  233. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  234. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  235. package/registry/progress-steps.json +18 -0
  236. package/registry/progress.json +17 -0
  237. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  238. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  239. package/registry/radio-group.json +18 -0
  240. package/registry/rating.json +18 -0
  241. package/registry/ring-carousel.json +15 -0
  242. package/registry/scratch-card.json +18 -0
  243. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  244. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  245. package/registry/search-bar.json +17 -0
  246. package/registry/search-results.json +30 -0
  247. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  248. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  249. package/registry/segmented-control.json +18 -0
  250. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  251. package/registry/select/files/src/lib/kit.tsx +219 -0
  252. package/registry/select.json +17 -0
  253. package/registry/settings.json +26 -0
  254. package/registry/shimmer-button.json +17 -0
  255. package/registry/sign-in.json +26 -0
  256. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  257. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  258. package/registry/skeleton.json +17 -0
  259. package/registry/slide-to-confirm.json +18 -0
  260. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  261. package/registry/slider/files/src/lib/kit.tsx +219 -0
  262. package/registry/slider.json +17 -0
  263. package/registry/spin-wheel.json +18 -0
  264. package/registry/spinner/files/src/components/ui/spinner.tsx +748 -0
  265. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  266. package/registry/spinner.json +17 -0
  267. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  268. package/registry/stateful-button/files/src/lib/kit.tsx +219 -0
  269. package/registry/stateful-button.json +18 -0
  270. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  271. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  272. package/registry/stepper.json +17 -0
  273. package/registry/swipe-deck.json +15 -0
  274. package/registry/swipeable-row.json +15 -0
  275. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  276. package/registry/switch/files/src/lib/kit.tsx +219 -0
  277. package/registry/switch.json +18 -0
  278. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  279. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  280. package/registry/tabs.json +17 -0
  281. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  282. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  283. package/registry/tag-input.json +17 -0
  284. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  285. package/registry/text-animation/files/src/lib/kit.tsx +219 -0
  286. package/registry/text-animation.json +15 -0
  287. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  288. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  289. package/registry/textarea.json +17 -0
  290. package/registry/ticket-card.json +24 -0
  291. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  292. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  293. package/registry/ticket-shape.json +17 -0
  294. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  295. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  296. package/registry/tilt-card.json +17 -0
  297. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  298. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  299. package/registry/time-picker.json +17 -0
  300. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  301. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  302. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  303. package/registry/timeline-chart.json +14 -0
  304. package/registry/timeline.json +20 -0
  305. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  306. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  307. package/registry/toast/files/src/lib/kit.tsx +219 -0
  308. package/registry/toast.json +20 -0
  309. package/registry/treemap.json +14 -0
  310. package/registry/waterfall.json +14 -0
  311. package/registry/wheel-list.json +17 -0
  312. package/schema/components.schema.json +13 -0
@@ -0,0 +1,654 @@
1
+ import * as Haptics from 'expo-haptics';
2
+ import React, { useEffect, useId, useRef, useState } from 'react';
3
+ import {
4
+ AccessibilityInfo,
5
+ Animated,
6
+ Easing,
7
+ Pressable,
8
+ StyleSheet,
9
+ Text,
10
+ View,
11
+ type StyleProp,
12
+ type ViewStyle,
13
+ } from 'react-native';
14
+ import Svg, { Circle, Defs, Path, RadialGradient, Rect, Stop } from 'react-native-svg';
15
+
16
+ import { alpha, motion, readableOn, useKit } from '@/lib/kit';
17
+
18
+ export type AlertVariant = 'info' | 'success' | 'warning' | 'danger';
19
+ /**
20
+ * `soft` — card with a glow of the tone behind the icon (default).
21
+ * `outline` — transparent with a hairline tone border.
22
+ * `solid` — filled with the solid tone.
23
+ * @deprecated `accent-bar` — renders as `soft`; the side-bar look was retired.
24
+ */
25
+ export type AlertAppearance = 'soft' | 'outline' | 'solid' | 'accent-bar';
26
+
27
+ export interface AlertAction {
28
+ label: string;
29
+ onPress: () => void;
30
+ }
31
+
32
+ export interface AlertActions {
33
+ /** Filled pill tinted with the tone. */
34
+ primary: AlertAction;
35
+ /** Quiet neutral pill beside it. */
36
+ secondary?: AlertAction;
37
+ }
38
+
39
+ export interface AlertProps {
40
+ variant?: AlertVariant;
41
+ /** Surface look. `accent-bar` is deprecated and renders as `soft`. */
42
+ appearance?: AlertAppearance;
43
+ title?: string;
44
+ description?: React.ReactNode;
45
+ children?: React.ReactNode;
46
+ /** Custom icon (shown inside the halo), or `false` to hide it. */
47
+ icon?: React.ReactNode | false;
48
+ /** One action: a pill under the text, or inline on the right in the compact (no title) layout. Or any node. */
49
+ action?: AlertAction | React.ReactNode;
50
+ /** A primary + secondary pill pair on the bottom row. */
51
+ actions?: AlertActions;
52
+ /** Show a close button that collapses the alert away. */
53
+ dismissible?: boolean;
54
+ /** Controlled visibility. Changing it to false collapses the alert smoothly. */
55
+ visible?: boolean;
56
+ /** Called when closed (button or `duration`). In controlled mode, set `visible` to false here. */
57
+ onDismiss?: () => void;
58
+ /** Auto-dismiss after this many ms. A hairline along the bottom drains to show the time left; touching pauses it. */
59
+ duration?: number;
60
+ /** Play a notification haptic (success / warning / error) when the alert appears. */
61
+ haptics?: boolean;
62
+ /** Override the tone colour. */
63
+ color?: string;
64
+ style?: StyleProp<ViewStyle>;
65
+ }
66
+
67
+ const HALO = 34;
68
+ const HALO_COMPACT = 28;
69
+
70
+ /**
71
+ * Inline banner with a living icon in a halo, a soft glow of its tone, and optional actions.
72
+ * The icon draws itself in, an optional timer drains along the bottom edge (pausing while
73
+ * touched), and on dismiss the alert fades and collapses its height so the content below glides up.
74
+ */
75
+ export function Alert({
76
+ variant = 'info',
77
+ appearance = 'soft',
78
+ title,
79
+ description,
80
+ children,
81
+ icon,
82
+ action,
83
+ actions,
84
+ dismissible,
85
+ visible,
86
+ onDismiss,
87
+ duration,
88
+ haptics = false,
89
+ color,
90
+ style,
91
+ }: AlertProps) {
92
+ const { colors, radius, dark } = useKit();
93
+ const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
94
+ const reduceMotion = useReduceMotion();
95
+ const tones: Record<AlertVariant, string> = {
96
+ info: dark ? '#60a5fa' : '#2563eb',
97
+ success: colors.success,
98
+ warning: colors.warning,
99
+ danger: colors.danger,
100
+ };
101
+ const tone = color ?? tones[variant];
102
+ const look = appearance === 'accent-bar' ? 'soft' : appearance;
103
+ const solid = look === 'solid';
104
+
105
+ const [inner, setInner] = useState(true);
106
+ const open = visible ?? inner;
107
+ const [box, setBox] = useState({ w: 0, h: 0 });
108
+ const [animating, setAnimating] = useState(false);
109
+ const [gone, setGone] = useState(!open);
110
+
111
+ // Native: card life, icon glyph pop, halo ripple, timer drain. JS: wrapper height, icon outline draw.
112
+ const life = useRef(new Animated.Value(0)).current;
113
+ const size = useRef(new Animated.Value(open ? 1 : 0)).current;
114
+ const draw = useRef(new Animated.Value(0)).current;
115
+ const glyph = useRef(new Animated.Value(0)).current;
116
+ const ripple = useRef(new Animated.Value(0)).current;
117
+ const drain = useRef(new Animated.Value(1)).current;
118
+ const first = useRef(true);
119
+ const reduceRef = useRef(reduceMotion);
120
+ reduceRef.current = reduceMotion;
121
+
122
+ const dismiss = () => {
123
+ if (visible === undefined) setInner(false);
124
+ onDismiss?.();
125
+ };
126
+ const dismissRef = useRef(dismiss);
127
+ dismissRef.current = dismiss;
128
+
129
+ const playIcon = () => {
130
+ if (reduceRef.current) {
131
+ draw.setValue(1);
132
+ glyph.setValue(1);
133
+ ripple.setValue(1);
134
+ return;
135
+ }
136
+ draw.setValue(0);
137
+ glyph.setValue(0);
138
+ ripple.setValue(0);
139
+ Animated.timing(draw, { toValue: 1, duration: 520, easing: Easing.out(Easing.cubic), useNativeDriver: false }).start();
140
+ Animated.sequence([
141
+ Animated.delay(280),
142
+ Animated.spring(glyph, { toValue: 1, useNativeDriver: true, ...motion.bouncy }),
143
+ ]).start();
144
+ Animated.timing(ripple, { toValue: 1, duration: 700, delay: 120, easing: Easing.out(Easing.quad), useNativeDriver: true }).start();
145
+ };
146
+
147
+ const buzz = () => {
148
+ if (!haptics) return;
149
+ const type =
150
+ variant === 'success'
151
+ ? Haptics.NotificationFeedbackType.Success
152
+ : variant === 'warning'
153
+ ? Haptics.NotificationFeedbackType.Warning
154
+ : variant === 'danger'
155
+ ? Haptics.NotificationFeedbackType.Error
156
+ : null;
157
+ if (type !== null) Haptics.notificationAsync(type).catch(() => {});
158
+ };
159
+
160
+ useEffect(() => {
161
+ if (first.current) {
162
+ first.current = false;
163
+ if (open) {
164
+ Animated.spring(life, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start();
165
+ playIcon();
166
+ buzz();
167
+ }
168
+ return;
169
+ }
170
+ setAnimating(true);
171
+ if (open) {
172
+ setGone(false);
173
+ playIcon();
174
+ buzz();
175
+ Animated.parallel([
176
+ Animated.timing(size, { toValue: 1, duration: 260, easing: Easing.out(Easing.cubic), useNativeDriver: false }),
177
+ Animated.spring(life, { toValue: 1, useNativeDriver: true, ...motion.gentle }),
178
+ ]).start(() => setAnimating(false));
179
+ } else {
180
+ Animated.parallel([
181
+ Animated.timing(life, { toValue: 0, duration: 160, useNativeDriver: true }),
182
+ Animated.timing(size, {
183
+ toValue: 0,
184
+ duration: 280,
185
+ delay: 90,
186
+ easing: Easing.inOut(Easing.cubic),
187
+ useNativeDriver: false,
188
+ }),
189
+ ]).start(({ finished }) => {
190
+ if (!finished) return;
191
+ setAnimating(false);
192
+ setGone(true);
193
+ });
194
+ }
195
+ // eslint-disable-next-line react-hooks/exhaustive-deps
196
+ }, [open]);
197
+
198
+ // Reduce motion switched on mid-animation: jump the decorative icon motion to its end.
199
+ useEffect(() => {
200
+ if (!reduceMotion) return;
201
+ draw.stopAnimation();
202
+ draw.setValue(1);
203
+ glyph.setValue(1);
204
+ ripple.setValue(1);
205
+ }, [reduceMotion, draw, glyph, ripple]);
206
+
207
+ // Auto-dismiss timer. Wall-clock bookkeeping so pause/resume is exact without reading native values.
208
+ const remaining = useRef(0);
209
+ const startedAt = useRef(0);
210
+ const held = useRef({ press: false, hover: false });
211
+ const running = useRef(false);
212
+
213
+ const runTimer = () => {
214
+ if (!duration || remaining.current <= 0) return;
215
+ running.current = true;
216
+ startedAt.current = Date.now();
217
+ Animated.timing(drain, {
218
+ toValue: 0,
219
+ duration: remaining.current,
220
+ easing: Easing.linear,
221
+ useNativeDriver: true,
222
+ }).start(({ finished }) => {
223
+ if (finished) {
224
+ running.current = false;
225
+ dismissRef.current();
226
+ }
227
+ });
228
+ };
229
+
230
+ const hold = (key: 'press' | 'hover', on: boolean) => {
231
+ held.current[key] = on;
232
+ const paused = held.current.press || held.current.hover;
233
+ if (paused && running.current) {
234
+ running.current = false;
235
+ remaining.current -= Date.now() - startedAt.current;
236
+ drain.stopAnimation();
237
+ } else if (!paused && !running.current && open) {
238
+ runTimer();
239
+ }
240
+ };
241
+
242
+ useEffect(() => {
243
+ if (!open || !duration) return;
244
+ drain.setValue(1);
245
+ remaining.current = duration;
246
+ held.current = { press: false, hover: false };
247
+ runTimer();
248
+ return () => {
249
+ running.current = false;
250
+ drain.stopAnimation();
251
+ };
252
+ // eslint-disable-next-line react-hooks/exhaustive-deps
253
+ }, [open, duration]);
254
+
255
+ if (gone && !animating) return null;
256
+
257
+ const compact = !title && !actions;
258
+ const haloSize = compact ? HALO_COMPACT : HALO;
259
+ const pad = compact ? 10 : 14;
260
+ const ink = dark ? tone : mix(tone, colors.foreground, 0.28);
261
+
262
+ const fg = solid ? readableOn(tone) : colors.foreground;
263
+ const sub = solid ? alpha(fg, 0.82) : colors.mutedForeground;
264
+ const iconColor = solid ? fg : tone;
265
+ const disc = solid ? alpha('#ffffff', 0.24) : alpha(tone, 0.14);
266
+ const ring = solid ? alpha('#ffffff', 0.12) : alpha(tone, 0.08);
267
+
268
+ const surface: ViewStyle = solid
269
+ ? { backgroundColor: tone }
270
+ : look === 'outline'
271
+ ? { backgroundColor: 'transparent', borderWidth: 1, borderColor: alpha(tone, dark ? 0.45 : 0.38) }
272
+ : { backgroundColor: colors.card, borderWidth: 1, borderColor: alpha(colors.foreground, dark ? 0.1 : 0.08) };
273
+
274
+ // Only the soft look gets a light-leak, and it is the tone itself at low opacity.
275
+ const glow = look === 'soft' ? { c: tone, a: dark ? 0.18 : 0.12 } : null;
276
+ const glowCenter = pad + haloSize / 2;
277
+
278
+ const pillColors = {
279
+ primary: solid ? { bg: fg, fg: tone } : { bg: tone, fg: readableOn(tone) },
280
+ secondary: solid
281
+ ? { bg: alpha(fg, 0.16), fg }
282
+ : { bg: alpha(colors.foreground, dark ? 0.08 : 0.05), fg: colors.foreground },
283
+ tint: solid ? { bg: alpha(fg, 0.16), fg } : { bg: alpha(tone, dark ? 0.18 : 0.12), fg: ink },
284
+ };
285
+
286
+ const single = isAction(action) ? action : null;
287
+ const pair: AlertActions | null = actions ?? (single && !compact ? { primary: single } : null);
288
+
289
+ const iconNode =
290
+ icon === false ? null : (
291
+ <Halo size={haloSize} corner={radius.capsule} disc={disc} ring={ring} ripple={ripple}>
292
+ {icon ?? (
293
+ <LivingIcon variant={variant} color={iconColor} size={compact ? 16 : 20} draw={draw} glyph={glyph} />
294
+ )}
295
+ </Halo>
296
+ );
297
+
298
+ const closeNode = dismissible ? (
299
+ <Pressable
300
+ accessibilityRole="button"
301
+ accessibilityLabel="Dismiss"
302
+ hitSlop={8}
303
+ onPress={dismiss}
304
+ style={({ pressed }) => [
305
+ styles.close,
306
+ {
307
+ borderRadius: radius.capsule(28),
308
+ backgroundColor: solid ? alpha(fg, pressed ? 0.24 : 0.14) : alpha(colors.foreground, pressed ? 0.12 : 0.06),
309
+ marginTop: compact ? 0 : (HALO - 28) / 2,
310
+ },
311
+ ]}
312
+ >
313
+ <Svg width={12} height={12} viewBox="0 0 24 24">
314
+ <Path d="M6 6l12 12M18 6L6 18" stroke={solid ? fg : colors.mutedForeground} strokeWidth={2.6} strokeLinecap="round" />
315
+ </Svg>
316
+ </Pressable>
317
+ ) : null;
318
+
319
+ const textTop = compact || icon === false ? 0 : (HALO - (title ? 21 : 20)) / 2;
320
+
321
+ const row = (
322
+ <>
323
+ {iconNode}
324
+ <View style={[styles.body, { paddingTop: textTop }]}>
325
+ {title ? <Text style={[styles.title, { color: fg }]}>{title}</Text> : null}
326
+ {typeof description === 'string' ? (
327
+ <Text style={[styles.desc, { color: title ? sub : fg }]}>{description}</Text>
328
+ ) : (
329
+ description
330
+ )}
331
+ {children}
332
+ {pair ? (
333
+ <View style={styles.actions}>
334
+ <Pill action={pair.primary} height={32} radius={radius.capsule(32)} {...pillColors.primary} />
335
+ {pair.secondary ? (
336
+ <Pill action={pair.secondary} height={32} radius={radius.capsule(32)} {...pillColors.secondary} />
337
+ ) : null}
338
+ </View>
339
+ ) : !single && action ? (
340
+ (action as React.ReactNode)
341
+ ) : null}
342
+ </View>
343
+ {compact && single ? <Pill action={single} height={28} radius={radius.capsule(28)} {...pillColors.tint} /> : null}
344
+ {closeNode}
345
+ </>
346
+ );
347
+
348
+ const rowStyle = [styles.row, compact ? styles.rowCompact : null, { padding: pad }];
349
+
350
+ return (
351
+ <Animated.View
352
+ style={[
353
+ animating && box.h > 0
354
+ ? { height: size.interpolate({ inputRange: [0, 1], outputRange: [0, box.h] }), overflow: 'hidden' }
355
+ : null,
356
+ style,
357
+ ]}
358
+ >
359
+ <Animated.View
360
+ onLayout={(e) => setBox({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
361
+ accessibilityRole="alert"
362
+ accessibilityLiveRegion="polite"
363
+ style={[
364
+ styles.card,
365
+ { borderRadius: radius.scale(compact ? 14 : 16) },
366
+ surface,
367
+ {
368
+ opacity: life,
369
+ transform: [
370
+ { translateY: life.interpolate({ inputRange: [0, 1], outputRange: [-8, 0] }) },
371
+ { scale: life.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) },
372
+ ],
373
+ },
374
+ ]}
375
+ >
376
+ {box.w > 0 && glow && icon !== false ? (
377
+ <Svg width={box.w} height={box.h} style={StyleSheet.absoluteFill} pointerEvents="none">
378
+ <Defs>
379
+ <RadialGradient
380
+ id={`glow${uid}`}
381
+ cx={glowCenter}
382
+ cy={glowCenter}
383
+ fx={glowCenter}
384
+ fy={glowCenter}
385
+ r={Math.min(240, Math.max(120, box.w * 0.5))}
386
+ gradientUnits="userSpaceOnUse"
387
+ >
388
+ <Stop offset="0" stopColor={glow.c} stopOpacity={glow.a} />
389
+ <Stop offset="0.35" stopColor={glow.c} stopOpacity={glow.a * 0.4} />
390
+ <Stop offset="1" stopColor={glow.c} stopOpacity={0} />
391
+ </RadialGradient>
392
+ </Defs>
393
+ <Rect width={box.w} height={box.h} fill={`url(#glow${uid})`} />
394
+ </Svg>
395
+ ) : null}
396
+
397
+ {duration ? (
398
+ <Pressable
399
+ accessible={false}
400
+ focusable={false}
401
+ onPressIn={() => hold('press', true)}
402
+ onPressOut={() => hold('press', false)}
403
+ onHoverIn={() => hold('hover', true)}
404
+ onHoverOut={() => hold('hover', false)}
405
+ style={rowStyle}
406
+ >
407
+ {row}
408
+ </Pressable>
409
+ ) : (
410
+ <View style={rowStyle}>{row}</View>
411
+ )}
412
+
413
+ {duration && box.w > 0 ? (
414
+ <Animated.View
415
+ pointerEvents="none"
416
+ style={[
417
+ styles.timer,
418
+ {
419
+ backgroundColor: solid ? alpha(fg, 0.7) : tone,
420
+ transform: [
421
+ { translateX: drain.interpolate({ inputRange: [0, 1], outputRange: [-box.w / 2, 0] }) },
422
+ { scaleX: drain.interpolate({ inputRange: [0, 1], outputRange: [0.0001, 1] }) },
423
+ ],
424
+ },
425
+ ]}
426
+ />
427
+ ) : null}
428
+ </Animated.View>
429
+ </Animated.View>
430
+ );
431
+ }
432
+
433
+ function Pill({
434
+ action,
435
+ height,
436
+ radius,
437
+ bg,
438
+ fg,
439
+ }: {
440
+ action: AlertAction;
441
+ height: number;
442
+ radius: number;
443
+ bg: string;
444
+ fg: string;
445
+ }) {
446
+ return (
447
+ <Pressable
448
+ accessibilityRole="button"
449
+ accessibilityLabel={action.label}
450
+ onPress={action.onPress}
451
+ hitSlop={(44 - height) / 2}
452
+ style={({ pressed }) => [
453
+ styles.pill,
454
+ { height, borderRadius: radius, backgroundColor: bg, paddingHorizontal: height >= 32 ? 14 : 12 },
455
+ pressed ? { opacity: 0.75, transform: [{ scale: 0.97 }] } : null,
456
+ ]}
457
+ >
458
+ <Text style={[styles.pillText, { color: fg }]} numberOfLines={1}>
459
+ {action.label}
460
+ </Text>
461
+ </Pressable>
462
+ );
463
+ }
464
+
465
+ /** Tinted disc with a faint outer ring; the ring ripples out once when `ripple` runs 0 → 1. */
466
+ function Halo({
467
+ size,
468
+ corner,
469
+ disc,
470
+ ring,
471
+ ripple,
472
+ children,
473
+ }: {
474
+ size: number;
475
+ corner: (h: number) => number;
476
+ disc: string;
477
+ ring: string;
478
+ ripple: Animated.Value;
479
+ children: React.ReactNode;
480
+ }) {
481
+ const outer = size + 8;
482
+ return (
483
+ <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
484
+ <View
485
+ style={{
486
+ position: 'absolute',
487
+ width: outer,
488
+ height: outer,
489
+ borderRadius: corner(outer),
490
+ backgroundColor: ring,
491
+ }}
492
+ />
493
+ <Animated.View
494
+ pointerEvents="none"
495
+ style={{
496
+ position: 'absolute',
497
+ width: size,
498
+ height: size,
499
+ borderRadius: corner(size),
500
+ borderWidth: 1.5,
501
+ borderColor: disc,
502
+ opacity: ripple.interpolate({ inputRange: [0, 0.15, 1], outputRange: [0, 1, 0] }),
503
+ transform: [{ scale: ripple.interpolate({ inputRange: [0, 1], outputRange: [1, 1.9] }) }],
504
+ }}
505
+ />
506
+ <View
507
+ style={{
508
+ width: size,
509
+ height: size,
510
+ borderRadius: corner(size),
511
+ backgroundColor: disc,
512
+ alignItems: 'center',
513
+ justifyContent: 'center',
514
+ }}
515
+ >
516
+ {children}
517
+ </View>
518
+ </View>
519
+ );
520
+ }
521
+
522
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
523
+ const OUTLINE_LEN = 64;
524
+
525
+ function iconPaths(variant: AlertVariant) {
526
+ const outline =
527
+ variant === 'warning'
528
+ ? 'M10.3 4.2a2 2 0 0 1 3.4 0l7.1 12.6a2 2 0 0 1-1.7 3H4.9a2 2 0 0 1-1.7-3z'
529
+ : // Circle r=9 drawn from 12 o'clock so it sweeps in clockwise.
530
+ 'M12 3a9 9 0 1 1 0 18a9 9 0 1 1 0-18';
531
+ const mark =
532
+ variant === 'success'
533
+ ? 'M8 12.4l2.7 2.7L16.2 9.4'
534
+ : variant === 'danger'
535
+ ? 'M9.2 9.2l5.6 5.6M14.8 9.2l-5.6 5.6'
536
+ : variant === 'warning'
537
+ ? 'M12 9.5v4'
538
+ : 'M12 11v5';
539
+ const dot = variant === 'warning' ? { cx: 12, cy: 16.6 } : variant === 'info' ? { cx: 12, cy: 7.9 } : null;
540
+ return { outline, mark, dot };
541
+ }
542
+
543
+ /** Outline draws itself (JS-driven dash offset), then the glyph springs in (native scale). */
544
+ function LivingIcon({
545
+ variant,
546
+ color,
547
+ size,
548
+ draw,
549
+ glyph,
550
+ }: {
551
+ variant: AlertVariant;
552
+ color: string;
553
+ size: number;
554
+ draw: Animated.Value;
555
+ glyph: Animated.Value;
556
+ }) {
557
+ const { outline, mark, dot } = iconPaths(variant);
558
+ const s = { stroke: color, strokeWidth: 2.2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' };
559
+ return (
560
+ <View style={{ width: size, height: size }} pointerEvents="none">
561
+ <Svg width={size} height={size} viewBox="0 0 24 24" style={StyleSheet.absoluteFill}>
562
+ <AnimatedPath
563
+ d={outline}
564
+ {...s}
565
+ strokeDasharray={[OUTLINE_LEN, OUTLINE_LEN]}
566
+ strokeDashoffset={draw.interpolate({ inputRange: [0, 1], outputRange: [OUTLINE_LEN, 0] })}
567
+ />
568
+ </Svg>
569
+ <Animated.View
570
+ style={[
571
+ StyleSheet.absoluteFill,
572
+ {
573
+ opacity: glyph.interpolate({ inputRange: [0, 0.3, 1], outputRange: [0, 1, 1], extrapolate: 'clamp' }),
574
+ transform: [
575
+ { translateY: glyph.interpolate({ inputRange: [0, 1], outputRange: [-3, 0] }) },
576
+ { scale: glyph.interpolate({ inputRange: [0, 1], outputRange: [0.4, 1] }) },
577
+ ],
578
+ },
579
+ ]}
580
+ >
581
+ <Svg width={size} height={size} viewBox="0 0 24 24">
582
+ <Path d={mark} {...s} />
583
+ {dot ? <Circle cx={dot.cx} cy={dot.cy} r={1.25} fill={color} /> : null}
584
+ </Svg>
585
+ </Animated.View>
586
+ </View>
587
+ );
588
+ }
589
+
590
+ /** The four built-in icons, static, drawn with react-native-svg. */
591
+ export function AlertIcon({ variant, color, size = 20 }: { variant: AlertVariant; color: string; size?: number }) {
592
+ const { outline, mark, dot } = iconPaths(variant);
593
+ const s = { stroke: color, strokeWidth: 2.2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' };
594
+ return (
595
+ <Svg width={size} height={size} viewBox="0 0 24 24">
596
+ <Path d={outline} {...s} />
597
+ <Path d={mark} {...s} />
598
+ {dot ? <Circle cx={dot.cx} cy={dot.cy} r={1.25} fill={color} /> : null}
599
+ </Svg>
600
+ );
601
+ }
602
+
603
+ function isAction(a: unknown): a is AlertAction {
604
+ return !!a && typeof a === 'object' && 'label' in a && 'onPress' in a;
605
+ }
606
+
607
+ function useReduceMotion() {
608
+ const [reduce, setReduce] = useState(false);
609
+ useEffect(() => {
610
+ let alive = true;
611
+ AccessibilityInfo.isReduceMotionEnabled()
612
+ .then((v) => alive && setReduce(v))
613
+ .catch(() => {});
614
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
615
+ return () => {
616
+ alive = false;
617
+ sub.remove();
618
+ };
619
+ }, []);
620
+ return reduce;
621
+ }
622
+
623
+ function parseHex(hex: string): [number, number, number] | null {
624
+ const full = hex.length === 4 ? '#' + [...hex.slice(1)].map((c) => c + c).join('') : hex;
625
+ if (!/^#[0-9a-fA-F]{6}/.test(full)) return null;
626
+ const n = parseInt(full.slice(1, 7), 16);
627
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
628
+ }
629
+
630
+ function toHex(r: number, g: number, b: number) {
631
+ return '#' + [r, g, b].map((v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0')).join('');
632
+ }
633
+
634
+ /** Blend `a` toward `b` by t (0–1). Used to deepen tone text so it reads on light surfaces. */
635
+ function mix(a: string, b: string, t: number) {
636
+ const x = parseHex(a);
637
+ const y = parseHex(b);
638
+ if (!x || !y) return a;
639
+ return toHex(x[0] + (y[0] - x[0]) * t, x[1] + (y[1] - x[1]) * t, x[2] + (y[2] - x[2]) * t);
640
+ }
641
+
642
+ const styles = StyleSheet.create({
643
+ card: { overflow: 'hidden' },
644
+ row: { flexDirection: 'row', alignItems: 'flex-start', gap: 12 },
645
+ rowCompact: { alignItems: 'center', gap: 10 },
646
+ body: { flex: 1, gap: 3 },
647
+ title: { fontSize: 15, fontWeight: '400', lineHeight: 21, letterSpacing: -0.1 },
648
+ desc: { fontSize: 14, fontWeight: '400', lineHeight: 20 },
649
+ actions: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10 },
650
+ pill: { alignItems: 'center', justifyContent: 'center' },
651
+ pillText: { fontSize: 14, fontWeight: '400', lineHeight: 18 },
652
+ close: { width: 28, height: 28, alignItems: 'center', justifyContent: 'center' },
653
+ timer: { position: 'absolute', left: 0, right: 0, bottom: 0, height: 2 },
654
+ });