@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,799 @@
1
+ import React, { createContext, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ AccessibilityInfo,
4
+ Animated,
5
+ Easing,
6
+ StyleSheet,
7
+ useWindowDimensions,
8
+ View,
9
+ type DimensionValue,
10
+ type LayoutChangeEvent,
11
+ type StyleProp,
12
+ type ViewStyle,
13
+ } from 'react-native';
14
+ import Svg, { Circle, Defs, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
15
+
16
+ import { alpha, motion, useKit } from '@/lib/kit';
17
+
18
+ export type SkeletonVariant = 'shimmer' | 'wave' | 'pulse' | 'static';
19
+
20
+ export interface SkeletonProps {
21
+ width?: DimensionValue;
22
+ height?: DimensionValue;
23
+ /**
24
+ * Corner radius: a theme step, 'capsule' for bars that follow the kit radius (a pill at the
25
+ * default radius), 'full' for true pills / circles, or points.
26
+ */
27
+ radius?: 'sm' | 'md' | 'lg' | 'xl' | 'capsule' | 'full' | number;
28
+ /**
29
+ * 'shimmer' sweeps one angled light band across every block on screen, 'wave' brightens blocks
30
+ * one after another from top to bottom, 'pulse' breathes, 'static' does nothing.
31
+ */
32
+ variant?: SkeletonVariant;
33
+ /** Base colour of the block (default: a faint tint of the foreground). */
34
+ color?: string;
35
+ /** Centred overlay, e.g. a faint glyph (see `Skeleton.Media`). */
36
+ children?: React.ReactNode;
37
+ style?: StyleProp<ViewStyle>;
38
+ }
39
+
40
+ /* ------------------------------------------------------------------------------------------------
41
+ * One shared clock for every skeleton on screen. It runs 0 → 1 linearly on the native driver;
42
+ * each variant maps it through its own (sampled) curve, so all blocks stay in lock-step.
43
+ * --------------------------------------------------------------------------------------------- */
44
+
45
+ const CYCLE = 2100;
46
+ /** Share of the cycle the shimmer band spends travelling; the rest is a calm pause. */
47
+ const SWEEP = 0.7;
48
+ const ANGLE = (20 * Math.PI) / 180;
49
+ const TAN = Math.tan(ANGLE);
50
+ const COS = Math.cos(ANGLE);
51
+ const SIN = Math.sin(ANGLE);
52
+
53
+ const clock = new Animated.Value(0);
54
+ let users = 0;
55
+ let loop: Animated.CompositeAnimation | null = null;
56
+
57
+ function useClock(active: boolean) {
58
+ useEffect(() => {
59
+ if (!active) return;
60
+ users += 1;
61
+ if (users === 1) {
62
+ clock.setValue(0);
63
+ loop = Animated.loop(Animated.timing(clock, { toValue: 1, duration: CYCLE, easing: Easing.linear, useNativeDriver: true }));
64
+ loop.start();
65
+ }
66
+ return () => {
67
+ users -= 1;
68
+ if (users === 0) {
69
+ loop?.stop();
70
+ loop = null;
71
+ }
72
+ };
73
+ }, [active]);
74
+ return clock;
75
+ }
76
+
77
+ // Reduce motion, read once and shared by every block (one OS listener, not one per bar).
78
+ let reduceMotion = false;
79
+ const reduceSubs = new Set<(v: boolean) => void>();
80
+ let reduceSub: { remove: () => void } | null = null;
81
+
82
+ function useReduceMotion() {
83
+ const [reduce, setReduce] = useState(reduceMotion);
84
+ useEffect(() => {
85
+ reduceSubs.add(setReduce);
86
+ if (!reduceSub) {
87
+ const publish = (v: boolean) => {
88
+ reduceMotion = v;
89
+ reduceSubs.forEach((fn) => fn(v));
90
+ };
91
+ AccessibilityInfo.isReduceMotionEnabled().then(publish).catch(() => {});
92
+ reduceSub = AccessibilityInfo.addEventListener('reduceMotionChanged', publish);
93
+ }
94
+ return () => {
95
+ reduceSubs.delete(setReduce);
96
+ if (reduceSubs.size === 0) {
97
+ reduceSub?.remove();
98
+ reduceSub = null;
99
+ }
100
+ };
101
+ }, []);
102
+ return reduce;
103
+ }
104
+
105
+ /** Samples `f` on [from, to] so a curve can run on the native driver (which has no `easing`). */
106
+ function sample(f: (x: number) => number, from: number, to: number, n: number) {
107
+ const inputRange: number[] = [];
108
+ const outputRange: number[] = [];
109
+ for (let i = 0; i <= n; i++) {
110
+ const x = from + ((to - from) * i) / n;
111
+ inputRange.push(x);
112
+ outputRange.push(f(x));
113
+ }
114
+ return { inputRange, outputRange };
115
+ }
116
+
117
+ const easeSweep = Easing.inOut(Easing.sin);
118
+ const easeRise = Easing.out(Easing.cubic);
119
+
120
+ /* ------------------------------------------------------------------------------------------------
121
+ * Group: the frame one wave travels across. Without a group it is the window, which on a phone is
122
+ * the screen. Nested groups defer to the outermost one so a whole screen stays one wave.
123
+ * --------------------------------------------------------------------------------------------- */
124
+
125
+ interface Frame {
126
+ x: number;
127
+ y: number;
128
+ w: number;
129
+ h: number;
130
+ }
131
+
132
+ const GroupContext = createContext<{ frame: Frame | null } | null>(null);
133
+
134
+ function measure(node: View | null, e: LayoutChangeEvent, done: (f: Frame) => void) {
135
+ const { x, y, width, height } = e.nativeEvent.layout;
136
+ const fallback = () => done({ x, y, w: width, h: height });
137
+ if (!node || typeof node.measureInWindow !== 'function') return fallback();
138
+ node.measureInWindow((wx, wy) => {
139
+ if (Number.isFinite(wx) && Number.isFinite(wy)) done({ x: wx, y: wy, w: width, h: height });
140
+ else fallback();
141
+ });
142
+ }
143
+
144
+ export interface SkeletonGroupProps {
145
+ children?: React.ReactNode;
146
+ style?: StyleProp<ViewStyle>;
147
+ }
148
+
149
+ /**
150
+ * Optional frame for the shimmer / wave: every block inside lines up as one wave across this
151
+ * view instead of across the whole window. Useful for a card or a preview that is narrower than
152
+ * the window (tablets, web). `Skeleton.Nav` and `Skeleton.Reveal` set one up for you.
153
+ */
154
+ export function SkeletonGroup({ children, style }: SkeletonGroupProps) {
155
+ const parent = useContext(GroupContext);
156
+ const ref = useRef<View>(null);
157
+ const [frame, setFrame] = useState<Frame | null>(null);
158
+ const value = useMemo(() => ({ frame }), [frame]);
159
+ if (parent) return <View style={style}>{children}</View>;
160
+ return (
161
+ <View ref={ref} style={style} onLayout={(e) => measure(ref.current, e, setFrame)}>
162
+ <GroupContext.Provider value={value}>{children}</GroupContext.Provider>
163
+ </View>
164
+ );
165
+ }
166
+
167
+ /* ------------------------------------------------------------------------------------------------
168
+ * Block
169
+ * --------------------------------------------------------------------------------------------- */
170
+
171
+ /**
172
+ * Placeholder block for loading content. An angled, soft-edged light band sweeps across every
173
+ * block on screen as one wave (or blocks brighten one after another, or gently pulse), all from
174
+ * one shared native-driver clock. Static when the OS "reduce motion" setting is on.
175
+ * Compose with `Skeleton.Text`, `.Avatar`, `.Media`, `.List`, `.Card`, `.Nav` and `.Reveal`.
176
+ */
177
+ export function Skeleton({ width = '100%', height = 14, radius = 'sm', variant = 'shimmer', color, children, style }: SkeletonProps) {
178
+ const { colors, radius: r, dark } = useKit();
179
+ const reduce = useReduceMotion();
180
+ const mode: SkeletonVariant = reduce ? 'static' : variant;
181
+ const moving = mode !== 'static';
182
+ const t = useClock(moving);
183
+ const group = useContext(GroupContext);
184
+ const win = useWindowDimensions();
185
+ const ref = useRef<View>(null);
186
+ const [box, setBox] = useState<Frame | null>(null);
187
+ const gid = 'sk' + useId().replace(/[^a-zA-Z0-9]/g, '');
188
+
189
+ const base = color ?? alpha(colors.foreground, dark ? 0.11 : 0.09);
190
+ // Light mode: a whisper of white over a pale block. Dark mode: a touch more lift so it reads.
191
+ const shine = dark ? colors.foreground : '#ffffff';
192
+ const peak = dark ? (mode === 'wave' ? 0.07 : 0.11) : mode === 'wave' ? 0.5 : 0.62;
193
+ const borderRadius =
194
+ typeof radius === 'number'
195
+ ? radius
196
+ : radius === 'capsule'
197
+ ? r.capsule(typeof height === 'number' ? height : 14)
198
+ : r[radius];
199
+
200
+ // Where this block sits inside the wave's frame.
201
+ const fx = group?.frame?.x ?? 0;
202
+ const fy = group?.frame?.y ?? 0;
203
+ const W = group?.frame?.w || win.width;
204
+ const H = Math.min(group?.frame?.h || win.height, 900);
205
+ const dx = box ? box.x - fx : 0;
206
+ const dy = box ? Math.max(0, Math.min(H, box.y - fy + box.h / 2)) : 0;
207
+ const bh = box?.h ?? 0;
208
+ const B = Math.max(160, Math.min(320, W * 0.55));
209
+ const bw = B + bh * TAN;
210
+
211
+ const shimmerX = useMemo(() => {
212
+ if (mode !== 'shimmer') return null;
213
+ // Global position of the band's centre line at the top of the frame, then this block's slice.
214
+ const start = -B;
215
+ const end = W + H * TAN + B;
216
+ const local = (x: number) => start + (end - start) * easeSweep(x / SWEEP) - dy * TAN - dx - bw / 2;
217
+ const s = sample(local, 0, SWEEP, 24);
218
+ s.inputRange.push(1);
219
+ s.outputRange.push(s.outputRange[s.outputRange.length - 1]);
220
+ return t.interpolate(s);
221
+ }, [mode, B, W, H, dx, dy, bw, t]);
222
+
223
+ const waveOpacity = useMemo(() => {
224
+ if (mode !== 'wave') return null;
225
+ const phase = (dy / Math.max(1, H)) * 0.55;
226
+ const half = 0.24;
227
+ return t.interpolate(
228
+ sample(
229
+ (x) => {
230
+ const d = Math.abs(x - phase);
231
+ const dist = Math.min(d, 1 - d);
232
+ return dist < half ? peak * (0.5 + 0.5 * Math.cos((Math.PI * dist) / half)) : 0;
233
+ },
234
+ 0,
235
+ 1,
236
+ 36,
237
+ ),
238
+ );
239
+ }, [mode, dy, H, peak, t]);
240
+
241
+ const pulse = useMemo(
242
+ () =>
243
+ mode === 'pulse'
244
+ ? { opacity: t.interpolate(sample((x) => 1 - 0.5 * (0.5 - 0.5 * Math.cos(2 * Math.PI * x)), 0, 1, 24)) }
245
+ : null,
246
+ [mode, t],
247
+ );
248
+
249
+ const L = (B / 2) * COS;
250
+ const cx = bw / 2;
251
+ const cy = bh / 2;
252
+
253
+ return (
254
+ <Animated.View
255
+ ref={ref}
256
+ accessibilityElementsHidden
257
+ importantForAccessibility="no-hide-descendants"
258
+ onLayout={mode === 'shimmer' || mode === 'wave' ? (e) => measure(ref.current, e, setBox) : undefined}
259
+ style={[styles.block, { width, height, borderRadius, backgroundColor: base }, pulse, style]}
260
+ >
261
+ {shimmerX && box && bh > 0 ? (
262
+ <Animated.View pointerEvents="none" style={[styles.band, { width: bw, transform: [{ translateX: shimmerX }] }]}>
263
+ <Svg width={bw} height={bh}>
264
+ <Defs>
265
+ <LinearGradient
266
+ id={gid}
267
+ gradientUnits="userSpaceOnUse"
268
+ x1={cx - L * COS}
269
+ y1={cy - L * SIN}
270
+ x2={cx + L * COS}
271
+ y2={cy + L * SIN}
272
+ >
273
+ <Stop offset="0" stopColor={shine} stopOpacity={0} />
274
+ <Stop offset="0.22" stopColor={shine} stopOpacity={peak * 0.12} />
275
+ <Stop offset="0.4" stopColor={shine} stopOpacity={peak * 0.62} />
276
+ <Stop offset="0.5" stopColor={shine} stopOpacity={peak} />
277
+ <Stop offset="0.6" stopColor={shine} stopOpacity={peak * 0.62} />
278
+ <Stop offset="0.78" stopColor={shine} stopOpacity={peak * 0.12} />
279
+ <Stop offset="1" stopColor={shine} stopOpacity={0} />
280
+ </LinearGradient>
281
+ </Defs>
282
+ <Rect x="0" y="0" width={bw} height={bh} fill={`url(#${gid})`} />
283
+ </Svg>
284
+ </Animated.View>
285
+ ) : null}
286
+ {waveOpacity ? (
287
+ <Animated.View pointerEvents="none" style={[StyleSheet.absoluteFill, { backgroundColor: shine, opacity: waveOpacity }]} />
288
+ ) : null}
289
+ {children != null ? (
290
+ <View pointerEvents="none" style={styles.center}>
291
+ {children}
292
+ </View>
293
+ ) : null}
294
+ </Animated.View>
295
+ );
296
+ }
297
+
298
+ /* ------------------------------------------------------------------------------------------------
299
+ * Composites
300
+ * --------------------------------------------------------------------------------------------- */
301
+
302
+ export interface SkeletonTextProps {
303
+ /** Number of lines (default 3). */
304
+ lines?: number;
305
+ /** Line height in points (default 12). */
306
+ lineHeight?: number;
307
+ gap?: number;
308
+ /** Width of the last line, so it reads like a paragraph (default '60%'). */
309
+ lastLineWidth?: DimensionValue;
310
+ variant?: SkeletonVariant;
311
+ style?: StyleProp<ViewStyle>;
312
+ }
313
+
314
+ const RAGGED: DimensionValue[] = ['100%', '94%', '97%', '90%'];
315
+
316
+ /** Paragraph placeholder: `lines` bars with a slightly ragged right edge and a shorter last line. */
317
+ export function SkeletonText({ lines = 3, lineHeight = 12, gap = 10, lastLineWidth = '60%', variant, style }: SkeletonTextProps) {
318
+ return (
319
+ <View style={[{ gap }, style]}>
320
+ {Array.from({ length: lines }, (_, i) => (
321
+ <Skeleton
322
+ key={i}
323
+ variant={variant}
324
+ height={lineHeight}
325
+ radius="capsule"
326
+ width={i === lines - 1 && lines > 1 ? lastLineWidth : RAGGED[i % RAGGED.length]}
327
+ />
328
+ ))}
329
+ </View>
330
+ );
331
+ }
332
+
333
+ export interface SkeletonAvatarProps {
334
+ size?: number;
335
+ shape?: 'circle' | 'rounded';
336
+ variant?: SkeletonVariant;
337
+ style?: StyleProp<ViewStyle>;
338
+ }
339
+
340
+ /** Round (or rounded-square) avatar placeholder. */
341
+ export function SkeletonAvatar({ size = 40, shape = 'circle', variant, style }: SkeletonAvatarProps) {
342
+ const { radius } = useKit();
343
+ return (
344
+ <Skeleton
345
+ variant={variant}
346
+ width={size}
347
+ height={size}
348
+ radius={shape === 'circle' ? 'full' : radius.scale(Math.round(size * 0.28))}
349
+ style={style}
350
+ />
351
+ );
352
+ }
353
+
354
+ export interface SkeletonMediaProps {
355
+ /** Width / height ratio (default 16 / 9). Ignored when `height` is set. */
356
+ aspectRatio?: number;
357
+ /** Fixed height in points. */
358
+ height?: number;
359
+ width?: DimensionValue;
360
+ /** Draw a faint image glyph in the centre (default false). */
361
+ glyph?: boolean;
362
+ /** Glyph size in points (default 28). */
363
+ glyphSize?: number;
364
+ radius?: SkeletonProps['radius'];
365
+ variant?: SkeletonVariant;
366
+ style?: StyleProp<ViewStyle>;
367
+ }
368
+
369
+ /** Image / video placeholder (optionally with a faint picture glyph in the middle). */
370
+ export function SkeletonMedia({
371
+ aspectRatio = 16 / 9,
372
+ height,
373
+ width = '100%',
374
+ glyph = false,
375
+ glyphSize = 28,
376
+ radius = 'md',
377
+ variant,
378
+ style,
379
+ }: SkeletonMediaProps) {
380
+ const { colors, dark } = useKit();
381
+ const ink = alpha(colors.foreground, dark ? 0.2 : 0.14);
382
+ return (
383
+ <Skeleton
384
+ variant={variant}
385
+ width={width}
386
+ height={height ?? 'auto'}
387
+ radius={radius}
388
+ style={[height == null ? { aspectRatio } : null, style]}
389
+ >
390
+ {glyph ? (
391
+ <Svg width={glyphSize} height={glyphSize} viewBox="0 0 24 24" fill="none">
392
+ <Path
393
+ d="M3.5 7A3.5 3.5 0 0 1 7 3.5h10A3.5 3.5 0 0 1 20.5 7v10a3.5 3.5 0 0 1-3.5 3.5H7A3.5 3.5 0 0 1 3.5 17z"
394
+ stroke={ink}
395
+ strokeWidth={1.6}
396
+ />
397
+ <Circle cx={9} cy={9.2} r={1.7} fill={ink} />
398
+ <Path d="M4 17.2l4.6-4.4a1.6 1.6 0 0 1 2.2 0l2.4 2.3 1.6-1.5a1.6 1.6 0 0 1 2.2 0l3.4 3.2" stroke={ink} strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" />
399
+ </Svg>
400
+ ) : null}
401
+ </Skeleton>
402
+ );
403
+ }
404
+
405
+ export interface SkeletonListProps {
406
+ /** Number of rows (default 4). */
407
+ rows?: number;
408
+ /** Avatar size (default 44). 0 hides it. */
409
+ avatarSize?: number;
410
+ avatarShape?: 'circle' | 'rounded';
411
+ /** Trailing short bar, like a time stamp or price (default true). */
412
+ trailing?: boolean;
413
+ /** Hairline between rows (default false). */
414
+ dividers?: boolean;
415
+ variant?: SkeletonVariant;
416
+ style?: StyleProp<ViewStyle>;
417
+ }
418
+
419
+ const TITLE_W: DimensionValue[] = ['58%', '44%', '66%', '50%', '62%'];
420
+ const SUB_W: DimensionValue[] = ['86%', '74%', '80%', '68%', '90%'];
421
+
422
+ /** List rows: avatar, title + subtitle lines and a trailing pill. Widths vary so it reads as real data. */
423
+ export function SkeletonList({
424
+ rows = 4,
425
+ avatarSize = 44,
426
+ avatarShape = 'circle',
427
+ trailing = true,
428
+ dividers = false,
429
+ variant,
430
+ style,
431
+ }: SkeletonListProps) {
432
+ const { colors } = useKit();
433
+ return (
434
+ <View style={style}>
435
+ {Array.from({ length: rows }, (_, i) => (
436
+ <View
437
+ key={i}
438
+ style={[
439
+ styles.listRow,
440
+ dividers && i > 0 ? { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: colors.border } : null,
441
+ ]}
442
+ >
443
+ {avatarSize > 0 ? <SkeletonAvatar variant={variant} size={avatarSize} shape={avatarShape} /> : null}
444
+ <View style={styles.rowText}>
445
+ <Skeleton variant={variant} width={TITLE_W[i % TITLE_W.length]} height={13} radius="capsule" />
446
+ <Skeleton variant={variant} width={SUB_W[i % SUB_W.length]} height={11} radius="capsule" />
447
+ </View>
448
+ {trailing ? (
449
+ <View style={styles.trailing}>
450
+ <Skeleton variant={variant} width={i % 3 === 1 ? 30 : 40} height={11} radius="capsule" />
451
+ </View>
452
+ ) : null}
453
+ </View>
454
+ ))}
455
+ </View>
456
+ );
457
+ }
458
+
459
+ export interface SkeletonCardProps {
460
+ /** Show the media block (default true). */
461
+ media?: boolean;
462
+ mediaHeight?: number;
463
+ /** Show the author row: avatar, name, handle · time, overflow menu (default true). */
464
+ header?: boolean;
465
+ /** Body text lines (default 2). */
466
+ lines?: number;
467
+ /** Like / comment / share row along the bottom (default true). */
468
+ actions?: boolean;
469
+ variant?: SkeletonVariant;
470
+ style?: StyleProp<ViewStyle>;
471
+ }
472
+
473
+ /** A feed post placeholder: author row, caption, media and an action bar — borderless, like the content it stands in for. */
474
+ export function SkeletonCard({
475
+ media = true,
476
+ mediaHeight = 168,
477
+ header = true,
478
+ lines = 2,
479
+ actions = true,
480
+ variant,
481
+ style,
482
+ }: SkeletonCardProps) {
483
+ return (
484
+ <View style={[styles.card, style]}>
485
+ {header ? (
486
+ <View style={styles.row}>
487
+ <SkeletonAvatar variant={variant} size={40} />
488
+ <View style={styles.rowText}>
489
+ <Skeleton variant={variant} width="46%" height={13} radius="capsule" />
490
+ <Skeleton variant={variant} width="28%" height={11} radius="capsule" />
491
+ </View>
492
+ </View>
493
+ ) : null}
494
+ {lines > 0 ? <SkeletonText variant={variant} lines={lines} lineHeight={12} gap={9} lastLineWidth="72%" /> : null}
495
+ {media ? <SkeletonMedia variant={variant} height={mediaHeight} radius="lg" /> : null}
496
+ {actions ? (
497
+ <View style={styles.actions}>
498
+ {[24, 20, 0].map((label, i) => (
499
+ <View key={i} style={styles.action}>
500
+ <Skeleton variant={variant} width={22} height={22} radius="full" />
501
+ {label ? <Skeleton variant={variant} width={label} height={10} radius="capsule" /> : null}
502
+ </View>
503
+ ))}
504
+ <View style={styles.flex} />
505
+ <Skeleton variant={variant} width={22} height={22} radius="full" />
506
+ </View>
507
+ ) : null}
508
+ </View>
509
+ );
510
+ }
511
+
512
+ export interface SkeletonNavProps {
513
+ /** App bar: back + title + trailing actions (default true). */
514
+ header?: boolean;
515
+ /** Show a back chevron placeholder (default true when header is on). */
516
+ back?: boolean;
517
+ /** Trailing action pills in the header (default 2). */
518
+ actions?: number;
519
+ /** Bottom tab bar (default true). */
520
+ tabs?: boolean;
521
+ /** Number of bottom tabs (default 4). */
522
+ tabCount?: number;
523
+ /** Body: how many list rows to show while content loads (default 4). 0 = empty body. */
524
+ rows?: number;
525
+ /** Custom body instead of the default list rows. */
526
+ children?: React.ReactNode;
527
+ variant?: SkeletonVariant;
528
+ style?: StyleProp<ViewStyle>;
529
+ }
530
+
531
+ /**
532
+ * Full-screen loading chrome for mobile: app header + body placeholders + bottom tabs.
533
+ * Keep the nav fixed while whatever the screen fetches paints in the middle.
534
+ */
535
+ export function SkeletonNav({
536
+ header = true,
537
+ back = true,
538
+ actions = 2,
539
+ tabs = true,
540
+ tabCount = 4,
541
+ rows = 4,
542
+ children,
543
+ variant,
544
+ style,
545
+ }: SkeletonNavProps) {
546
+ const { colors } = useKit();
547
+ return (
548
+ <SkeletonGroup style={[styles.navScreen, { backgroundColor: colors.background }, style]}>
549
+ {header ? (
550
+ <View style={styles.navHeader}>
551
+ {back ? <Skeleton variant={variant} width={32} height={32} radius="full" /> : <View style={{ width: 32 }} />}
552
+ <View style={styles.flex}>
553
+ <Skeleton variant={variant} width="45%" height={16} radius="capsule" />
554
+ </View>
555
+ <View style={styles.navActions}>
556
+ {Array.from({ length: Math.max(0, actions) }, (_, i) => (
557
+ <Skeleton key={i} variant={variant} width={32} height={32} radius="full" />
558
+ ))}
559
+ </View>
560
+ </View>
561
+ ) : null}
562
+
563
+ <View style={styles.navBody}>
564
+ {children ?? (rows > 0 ? <SkeletonList variant={variant} rows={rows} avatarShape="rounded" avatarSize={40} /> : null)}
565
+ </View>
566
+
567
+ {tabs ? (
568
+ <View style={[styles.navTabs, { borderTopColor: colors.border }]}>
569
+ {Array.from({ length: Math.max(2, tabCount) }, (_, i) => (
570
+ <View key={i} style={styles.navTab}>
571
+ <Skeleton variant={variant} width={24} height={24} radius="full" />
572
+ <Skeleton variant={variant} width={i === 0 ? 30 : 24} height={8} radius="capsule" />
573
+ </View>
574
+ ))}
575
+ </View>
576
+ ) : null}
577
+ </SkeletonGroup>
578
+ );
579
+ }
580
+
581
+ /* ------------------------------------------------------------------------------------------------
582
+ * Reveal: skeleton → content hand-off
583
+ * --------------------------------------------------------------------------------------------- */
584
+
585
+ export interface SkeletonRevealProps {
586
+ /** While true the skeleton shows; flip to false when the data arrives. */
587
+ loading: boolean;
588
+ /** Placeholder shown while loading (default: three text lines). */
589
+ skeleton?: React.ReactNode;
590
+ /** The real content. Top-level children rise in one after another. */
591
+ children?: React.ReactNode;
592
+ /** Delay between each top-level child rising in, in ms (default 70). 0 reveals it as one block. */
593
+ stagger?: number;
594
+ /** Glide the container height from the skeleton's height to the content's (default true). */
595
+ animateHeight?: boolean;
596
+ /** Screen reader label while loading (default 'Loading'). */
597
+ loadingLabel?: string;
598
+ style?: StyleProp<ViewStyle>;
599
+ }
600
+
601
+ type Layer = 'skeleton' | 'content';
602
+ const RISE = 380;
603
+ const RISE_Y = 10;
604
+ const FADE_OUT = 220;
605
+ const MAX_STAGGERED = 8;
606
+
607
+ /**
608
+ * Swaps a skeleton for real content without a jump: the skeleton dissolves, the content rises
609
+ * in child by child (cross-fade + translateY), and the container glides between the two heights.
610
+ * Instant when the OS "reduce motion" setting is on.
611
+ */
612
+ export function SkeletonReveal({
613
+ loading,
614
+ skeleton,
615
+ children,
616
+ stagger = 70,
617
+ animateHeight = true,
618
+ loadingLabel = 'Loading',
619
+ style,
620
+ }: SkeletonRevealProps) {
621
+ const reduce = useReduceMotion();
622
+ const [mode, setMode] = useState<Layer>(loading ? 'skeleton' : 'content');
623
+ const [leaving, setLeaving] = useState<Layer | null>(null);
624
+ const [fixed, setFixed] = useState(false);
625
+ const heights = useRef<Record<Layer, number>>({ skeleton: 0, content: 0 });
626
+ const pending = useRef(false);
627
+ const token = useRef(0);
628
+ const h = useRef(new Animated.Value(0)).current; // JS driver: container height
629
+ const skA = useRef(new Animated.Value(loading ? 1 : 0)).current; // native: skeleton layer
630
+ const ctA = useRef(new Animated.Value(loading ? 0 : 1)).current; // native: content layer
631
+
632
+ const items = React.Children.toArray(children);
633
+ const staggered = stagger > 0 && items.length > 1;
634
+ const steps = Math.min(items.length, MAX_STAGGERED);
635
+ const total = RISE + (staggered ? stagger * (steps - 1) : 0);
636
+ const value = (l: Layer) => (l === 'skeleton' ? skA : ctA);
637
+
638
+ const run = (next: Layer, prev: Layer) => {
639
+ const id = ++token.current;
640
+ const anims: Animated.CompositeAnimation[] = [
641
+ Animated.timing(value(prev), { toValue: 0, duration: FADE_OUT, easing: Easing.linear, useNativeDriver: true }),
642
+ Animated.timing(value(next), {
643
+ toValue: 1,
644
+ duration: next === 'content' ? total : FADE_OUT,
645
+ easing: Easing.linear,
646
+ useNativeDriver: true,
647
+ }),
648
+ ];
649
+ if (animateHeight) {
650
+ anims.push(
651
+ Animated.spring(h, { toValue: heights.current[next], ...motion.gentle, overshootClamping: true, useNativeDriver: false }),
652
+ );
653
+ }
654
+ Animated.parallel(anims).start(({ finished }) => {
655
+ if (!finished || id !== token.current) return;
656
+ setFixed(false);
657
+ setLeaving(null);
658
+ });
659
+ };
660
+
661
+ useEffect(() => {
662
+ const next: Layer = loading ? 'skeleton' : 'content';
663
+ if (next === mode) return;
664
+ const prev = mode;
665
+ if (reduce) {
666
+ token.current += 1;
667
+ value(prev).setValue(0);
668
+ value(next).setValue(1);
669
+ setFixed(false);
670
+ setLeaving(null);
671
+ setMode(next);
672
+ return;
673
+ }
674
+ if (animateHeight) {
675
+ h.stopAnimation();
676
+ if (!fixed) h.setValue(heights.current[prev]);
677
+ setFixed(true);
678
+ }
679
+ const wasMounted = leaving === next;
680
+ setLeaving(prev);
681
+ setMode(next);
682
+ if (wasMounted && heights.current[next] > 0) {
683
+ run(next, prev);
684
+ } else {
685
+ value(next).setValue(0);
686
+ pending.current = true;
687
+ }
688
+ // eslint-disable-next-line react-hooks/exhaustive-deps
689
+ }, [loading]);
690
+
691
+ const onLayout = (l: Layer) => (e: LayoutChangeEvent) => {
692
+ heights.current[l] = e.nativeEvent.layout.height;
693
+ if (l === mode && pending.current) {
694
+ pending.current = false;
695
+ run(mode, l === 'skeleton' ? 'content' : 'skeleton');
696
+ }
697
+ };
698
+
699
+ const rise = (i: number) => {
700
+ const k = Math.min(i, MAX_STAGGERED - 1);
701
+ const a = (k * stagger) / total;
702
+ const b = Math.min(1, (k * stagger + RISE) / total);
703
+ const p = sample((x) => easeRise((x - a) / (b - a)), a, b, 10);
704
+ if (a > 0) {
705
+ p.inputRange.unshift(0);
706
+ p.outputRange.unshift(0);
707
+ }
708
+ if (b < 1) {
709
+ p.inputRange.push(1);
710
+ p.outputRange.push(1);
711
+ }
712
+ const v = ctA.interpolate(p);
713
+ return { opacity: v, transform: [{ translateY: v.interpolate({ inputRange: [0, 1], outputRange: [RISE_Y, 0] }) }] };
714
+ };
715
+
716
+ const layerStyle = (l: Layer) => (l === mode ? null : styles.leaving);
717
+ const showSkeleton = mode === 'skeleton' || leaving === 'skeleton';
718
+ const showContent = mode === 'content' || leaving === 'content';
719
+
720
+ return (
721
+ <Animated.View
722
+ accessibilityState={{ busy: mode === 'skeleton' }}
723
+ style={[fixed ? { height: h, overflow: 'hidden' } : null, style]}
724
+ >
725
+ {showSkeleton ? (
726
+ <Animated.View
727
+ accessible={mode === 'skeleton'}
728
+ accessibilityRole="progressbar"
729
+ accessibilityLabel={loadingLabel}
730
+ pointerEvents="none"
731
+ onLayout={onLayout('skeleton')}
732
+ style={[layerStyle('skeleton'), { opacity: skA }]}
733
+ >
734
+ <SkeletonGroup>{skeleton ?? <SkeletonText lines={3} />}</SkeletonGroup>
735
+ </Animated.View>
736
+ ) : null}
737
+ {showContent ? (
738
+ <Animated.View
739
+ accessibilityLiveRegion="polite"
740
+ pointerEvents={mode === 'content' ? 'auto' : 'none'}
741
+ onLayout={onLayout('content')}
742
+ style={[layerStyle('content'), staggered ? null : rise(0)]}
743
+ >
744
+ {staggered
745
+ ? items.map((child, i) => (
746
+ <Animated.View key={i} style={rise(i)}>
747
+ {child}
748
+ </Animated.View>
749
+ ))
750
+ : children}
751
+ </Animated.View>
752
+ ) : null}
753
+ </Animated.View>
754
+ );
755
+ }
756
+
757
+ Skeleton.Text = SkeletonText;
758
+ Skeleton.Avatar = SkeletonAvatar;
759
+ Skeleton.Media = SkeletonMedia;
760
+ Skeleton.List = SkeletonList;
761
+ Skeleton.Card = SkeletonCard;
762
+ Skeleton.Nav = SkeletonNav;
763
+ Skeleton.Group = SkeletonGroup;
764
+ Skeleton.Reveal = SkeletonReveal;
765
+
766
+ const styles = StyleSheet.create({
767
+ block: { overflow: 'hidden' },
768
+ band: { position: 'absolute', top: 0, bottom: 0, left: 0 },
769
+ center: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, alignItems: 'center', justifyContent: 'center' },
770
+ flex: { flex: 1 },
771
+ card: { gap: 14 },
772
+ row: { flexDirection: 'row', alignItems: 'center', gap: 12 },
773
+ rowText: { flex: 1, gap: 8 },
774
+ actions: { flexDirection: 'row', alignItems: 'center', gap: 18 },
775
+ action: { flexDirection: 'row', alignItems: 'center', gap: 7 },
776
+ listRow: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 12 },
777
+ trailing: { alignItems: 'flex-end', minWidth: 40 },
778
+ leaving: { position: 'absolute', top: 0, left: 0, right: 0 },
779
+ navScreen: { flex: 1, alignSelf: 'stretch', minHeight: 320, overflow: 'hidden' },
780
+ navHeader: {
781
+ flexDirection: 'row',
782
+ alignItems: 'center',
783
+ gap: 12,
784
+ paddingHorizontal: 16,
785
+ paddingVertical: 12,
786
+ },
787
+ navActions: { flexDirection: 'row', alignItems: 'center', gap: 10 },
788
+ navBody: { flex: 1, paddingHorizontal: 16 },
789
+ navTabs: {
790
+ flexDirection: 'row',
791
+ alignItems: 'center',
792
+ justifyContent: 'space-around',
793
+ paddingHorizontal: 8,
794
+ paddingTop: 12,
795
+ paddingBottom: 16,
796
+ borderTopWidth: StyleSheet.hairlineWidth,
797
+ },
798
+ navTab: { alignItems: 'center', gap: 6, minWidth: 44 },
799
+ });