panelui-native 0.22.6 → 0.25.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 (135) hide show
  1. package/README.md +1 -0
  2. package/lib/module/components/area-chart/index.js +826 -0
  3. package/lib/module/components/area-chart/index.js.map +1 -0
  4. package/lib/module/components/avatar/index.js +1 -1
  5. package/lib/module/components/avatar/index.js.map +1 -1
  6. package/lib/module/components/bar-chart/index.js +853 -0
  7. package/lib/module/components/bar-chart/index.js.map +1 -0
  8. package/lib/module/components/bottom-sheet/index.js +207 -23
  9. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  10. package/lib/module/components/breadcrumb/index.js +275 -0
  11. package/lib/module/components/breadcrumb/index.js.map +1 -0
  12. package/lib/module/components/chip/index.js +2 -2
  13. package/lib/module/components/direction/index.js +13 -19
  14. package/lib/module/components/direction/index.js.map +1 -1
  15. package/lib/module/components/frame/index.js +17 -0
  16. package/lib/module/components/frame/index.js.map +1 -1
  17. package/lib/module/components/heatmap-chart/index.js +28 -8
  18. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  19. package/lib/module/components/input-group/index.js +2 -2
  20. package/lib/module/components/input-group/index.js.map +1 -1
  21. package/lib/module/components/line-chart/index.js +4 -146
  22. package/lib/module/components/line-chart/index.js.map +1 -1
  23. package/lib/module/components/loader/index.js +758 -0
  24. package/lib/module/components/loader/index.js.map +1 -0
  25. package/lib/module/components/map/basemap.js +265 -0
  26. package/lib/module/components/map/basemap.js.map +1 -0
  27. package/lib/module/components/map/index.js +850 -0
  28. package/lib/module/components/map/index.js.map +1 -0
  29. package/lib/module/components/map/maplibre.js +87 -0
  30. package/lib/module/components/map/maplibre.js.map +1 -0
  31. package/lib/module/components/message/index.js +8 -7
  32. package/lib/module/components/message/index.js.map +1 -1
  33. package/lib/module/components/otp-input/index.js +2 -2
  34. package/lib/module/components/progress/index.js +8 -2
  35. package/lib/module/components/progress/index.js.map +1 -1
  36. package/lib/module/components/ring-chart/index.js +423 -0
  37. package/lib/module/components/ring-chart/index.js.map +1 -0
  38. package/lib/module/components/section-rail/index.js +2 -2
  39. package/lib/module/components/separator/index.js +76 -22
  40. package/lib/module/components/separator/index.js.map +1 -1
  41. package/lib/module/components/shimmer/index.js +10 -1
  42. package/lib/module/components/shimmer/index.js.map +1 -1
  43. package/lib/module/components/slider/index.js +19 -5
  44. package/lib/module/components/slider/index.js.map +1 -1
  45. package/lib/module/components/steps/index.js +1 -1
  46. package/lib/module/components/switch/index.js +6 -1
  47. package/lib/module/components/switch/index.js.map +1 -1
  48. package/lib/module/components/typography/index.js +5 -0
  49. package/lib/module/components/typography/index.js.map +1 -1
  50. package/lib/module/hooks/use-direction.js +43 -0
  51. package/lib/module/hooks/use-direction.js.map +1 -0
  52. package/lib/module/icons/index.js +117 -2
  53. package/lib/module/icons/index.js.map +1 -1
  54. package/lib/module/index.js +8 -2
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/primitives/text.js +30 -10
  57. package/lib/module/primitives/text.js.map +1 -1
  58. package/lib/module/utils/chart.js +279 -0
  59. package/lib/module/utils/chart.js.map +1 -0
  60. package/lib/typescript/src/components/area-chart/index.d.ts +197 -0
  61. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -0
  62. package/lib/typescript/src/components/bar-chart/index.d.ts +228 -0
  63. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -0
  64. package/lib/typescript/src/components/bottom-sheet/index.d.ts +75 -4
  65. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  66. package/lib/typescript/src/components/breadcrumb/index.d.ts +100 -0
  67. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -0
  68. package/lib/typescript/src/components/direction/index.d.ts +11 -10
  69. package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
  70. package/lib/typescript/src/components/frame/index.d.ts +17 -0
  71. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  72. package/lib/typescript/src/components/heatmap-chart/index.d.ts +22 -3
  73. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  74. package/lib/typescript/src/components/line-chart/index.d.ts +1 -0
  75. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  76. package/lib/typescript/src/components/loader/index.d.ts +38 -0
  77. package/lib/typescript/src/components/loader/index.d.ts.map +1 -0
  78. package/lib/typescript/src/components/map/basemap.d.ts +76 -0
  79. package/lib/typescript/src/components/map/basemap.d.ts.map +1 -0
  80. package/lib/typescript/src/components/map/index.d.ts +367 -0
  81. package/lib/typescript/src/components/map/index.d.ts.map +1 -0
  82. package/lib/typescript/src/components/map/maplibre.d.ts +193 -0
  83. package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -0
  84. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/ring-chart/index.d.ts +156 -0
  87. package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -0
  88. package/lib/typescript/src/components/separator/index.d.ts +36 -1
  89. package/lib/typescript/src/components/separator/index.d.ts.map +1 -1
  90. package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/typography/index.d.ts +6 -0
  94. package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
  95. package/lib/typescript/src/hooks/use-direction.d.ts +20 -0
  96. package/lib/typescript/src/hooks/use-direction.d.ts.map +1 -0
  97. package/lib/typescript/src/icons/index.d.ts +5 -0
  98. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  99. package/lib/typescript/src/index.d.ts +9 -3
  100. package/lib/typescript/src/index.d.ts.map +1 -1
  101. package/lib/typescript/src/primitives/text.d.ts.map +1 -1
  102. package/lib/typescript/src/utils/chart.d.ts +96 -0
  103. package/lib/typescript/src/utils/chart.d.ts.map +1 -0
  104. package/package.json +9 -1
  105. package/src/components/area-chart/index.tsx +994 -0
  106. package/src/components/avatar/index.tsx +1 -1
  107. package/src/components/bar-chart/index.tsx +1048 -0
  108. package/src/components/bottom-sheet/index.tsx +280 -20
  109. package/src/components/breadcrumb/index.tsx +365 -0
  110. package/src/components/chip/index.tsx +2 -2
  111. package/src/components/direction/index.tsx +20 -21
  112. package/src/components/frame/index.tsx +17 -0
  113. package/src/components/heatmap-chart/index.tsx +58 -11
  114. package/src/components/input-group/index.tsx +2 -2
  115. package/src/components/line-chart/index.tsx +13 -181
  116. package/src/components/loader/index.tsx +812 -0
  117. package/src/components/map/basemap.ts +304 -0
  118. package/src/components/map/index.tsx +1118 -0
  119. package/src/components/map/maplibre.ts +201 -0
  120. package/src/components/message/index.tsx +8 -7
  121. package/src/components/otp-input/index.tsx +2 -2
  122. package/src/components/progress/index.tsx +10 -2
  123. package/src/components/ring-chart/index.tsx +543 -0
  124. package/src/components/section-rail/index.tsx +2 -2
  125. package/src/components/separator/index.tsx +77 -19
  126. package/src/components/shimmer/index.tsx +10 -1
  127. package/src/components/slider/index.tsx +20 -5
  128. package/src/components/steps/index.tsx +1 -1
  129. package/src/components/switch/index.tsx +6 -1
  130. package/src/components/typography/index.tsx +5 -0
  131. package/src/hooks/use-direction.ts +45 -0
  132. package/src/icons/index.tsx +66 -0
  133. package/src/index.ts +84 -0
  134. package/src/primitives/text.tsx +25 -7
  135. package/src/utils/chart.ts +355 -0
@@ -0,0 +1,758 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Loading animations.
5
+ *
6
+ * ## Why one component and not nine
7
+ *
8
+ * These are interchangeable. A loader is chosen once, for the character it
9
+ * gives a screen, and then it is the same loader everywhere — so the thing
10
+ * that varies is a value, not a component. `variant` keeps the swap to one
11
+ * word and keeps every one of them on the same props: the same colour
12
+ * resolution, the same `speed`, the same reduced-motion behaviour, the same
13
+ * accessibility. Nine exports would mean nine chances for those to drift.
14
+ *
15
+ * ## Why they are drawn the way they are
16
+ *
17
+ * Anything with a fixed, small number of moving pieces is a view per piece
18
+ * with a `useAnimatedStyle` on it: three dots is three transforms a frame, and
19
+ * that is cheaper than rebuilding a path string.
20
+ *
21
+ * Anything whose count is high, or whose shape is derived rather than
22
+ * declared, is a single SVG path rebuilt on the UI thread. Fifteen bars
23
+ * following a bouncing ball is one animated prop that way and fifteen the
24
+ * other, and the fifteen do not stay in step with each other for free.
25
+ *
26
+ * ## Reduced motion
27
+ *
28
+ * Every variant draws a representative still frame rather than freezing at
29
+ * whatever it happened to be showing. A stopped animation should look like a
30
+ * shape someone drew, not like a bug caught mid-cycle.
31
+ */
32
+ import { memo, useEffect } from 'react';
33
+ import { View } from 'react-native';
34
+ import Svg, { Ellipse, Path } from 'react-native-svg';
35
+ import Animated, { Easing, cancelAnimation, useAnimatedProps, useAnimatedStyle, useFrameCallback, useReducedMotion, useSharedValue, withDelay, withRepeat, withTiming } from 'react-native-reanimated';
36
+ import { useCSSVariable } from 'uniwind';
37
+ import { tv } from 'tailwind-variants';
38
+ import { useIconColor } from "../../icons/index.js";
39
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
40
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
41
+ const AnimatedEllipse = Animated.createAnimatedComponent(Ellipse);
42
+ const loaderVariants = tv({
43
+ base: 'items-center justify-center'
44
+ });
45
+ /** Geometry at `md`. Every number here is multiplied by the size scale. */
46
+ const GEOMETRY = {
47
+ 'pulse-dots': {
48
+ dot: 10,
49
+ gap: 6,
50
+ count: 3
51
+ },
52
+ 'bounce-dots': {
53
+ dot: 10,
54
+ gap: 6,
55
+ count: 3,
56
+ travel: 8
57
+ },
58
+ 'pulsating-dots': {
59
+ dot: 11,
60
+ gap: 8,
61
+ count: 3
62
+ },
63
+ 'liquid-dots': {
64
+ radius: 11,
65
+ travel: 15,
66
+ width: 72,
67
+ height: 32
68
+ },
69
+ 'bar-cascade': {
70
+ bar: 5,
71
+ gap: 4,
72
+ count: 5,
73
+ min: 8,
74
+ max: 26
75
+ },
76
+ 'bouncing-bars': {
77
+ bar: 5,
78
+ gap: 6,
79
+ count: 3,
80
+ height: 30
81
+ },
82
+ 'symmetric-wave': {
83
+ bar: 5,
84
+ gap: 5,
85
+ count: 9,
86
+ min: 7,
87
+ max: 26
88
+ },
89
+ 'morph-ring': {
90
+ box: 34,
91
+ border: 3
92
+ },
93
+ 'wave-physics': {
94
+ bar: 5,
95
+ gap: 3,
96
+ count: 15,
97
+ base: 7,
98
+ peak: 26
99
+ }
100
+ };
101
+ const SIZE_SCALE = {
102
+ sm: 0.75,
103
+ md: 1,
104
+ lg: 1.35
105
+ };
106
+
107
+ /**
108
+ * A theme token rather than a colour. Tokens are named, not written, so the
109
+ * leading `--` tells the two apart without the caller having to say which.
110
+ */
111
+ function isToken(value) {
112
+ return typeof value === 'string' && value.startsWith('--');
113
+ }
114
+
115
+ /** One decimal is past the point a path string can show, and cheaper to build. */
116
+ function q(value) {
117
+ 'worklet';
118
+
119
+ return Math.round(value * 10) / 10;
120
+ }
121
+
122
+ /** A closed circle as a path subpath, so several can share one `Path`. */
123
+ function circle(cx, cy, r) {
124
+ 'worklet';
125
+
126
+ return `M${q(cx - r)},${q(cy)}` + `A${q(r)},${q(r)} 0 1 0 ${q(cx + r)},${q(cy)}` + `A${q(r)},${q(r)} 0 1 0 ${q(cx - r)},${q(cy)}Z`;
127
+ }
128
+
129
+ /* -------------------------------------------------------------------------- */
130
+ /* dots */
131
+ /* -------------------------------------------------------------------------- */
132
+
133
+ /**
134
+ * One dot.
135
+ *
136
+ * A component rather than a loop of `useAnimatedStyle` inside the parent: the
137
+ * count is geometry, and a loop of hooks is a rule waiting to be broken by
138
+ * whoever changes it.
139
+ */
140
+ const Dot = /*#__PURE__*/memo(function Dot({
141
+ size,
142
+ color,
143
+ delay,
144
+ duration,
145
+ from,
146
+ to,
147
+ property,
148
+ still,
149
+ animate
150
+ }) {
151
+ const progress = useSharedValue(animate ? from : still);
152
+ useEffect(() => {
153
+ if (!animate) {
154
+ progress.value = still;
155
+ return;
156
+ }
157
+ progress.value = from;
158
+ progress.value = withDelay(delay,
159
+ // Reversing rather than a three-stop keyframe: every one of these
160
+ // animations returns to where it started, and a reversed repeat says so
161
+ // in one line without a midpoint to keep in step.
162
+ withRepeat(withTiming(to, {
163
+ duration,
164
+ easing: Easing.inOut(Easing.quad)
165
+ }), -1, true));
166
+ return () => cancelAnimation(progress);
167
+ }, [animate, delay, duration, from, to, still, progress]);
168
+ const style = useAnimatedStyle(() => {
169
+ if (property === 'opacity') return {
170
+ opacity: progress.value
171
+ };
172
+ if (property === 'translateY') {
173
+ return {
174
+ transform: [{
175
+ translateY: progress.value
176
+ }]
177
+ };
178
+ }
179
+ return {
180
+ opacity: 0.5 + (progress.value - 1) * 1,
181
+ transform: [{
182
+ scale: progress.value
183
+ }]
184
+ };
185
+ });
186
+ return /*#__PURE__*/_jsx(Animated.View, {
187
+ style: [{
188
+ width: size,
189
+ height: size,
190
+ borderRadius: size / 2,
191
+ backgroundColor: color
192
+ }, style]
193
+ });
194
+ });
195
+
196
+ /* -------------------------------------------------------------------------- */
197
+ /* bars */
198
+ /* -------------------------------------------------------------------------- */
199
+
200
+ /** One bar. Same reasoning as `Dot`. */
201
+ const Bar = /*#__PURE__*/memo(function Bar({
202
+ width,
203
+ color,
204
+ delay,
205
+ duration,
206
+ from,
207
+ to,
208
+ mode,
209
+ barHeight,
210
+ still,
211
+ animate
212
+ }) {
213
+ const progress = useSharedValue(animate ? from : still);
214
+ useEffect(() => {
215
+ if (!animate) {
216
+ progress.value = still;
217
+ return;
218
+ }
219
+ progress.value = from;
220
+ progress.value = withDelay(delay, withRepeat(withTiming(to, {
221
+ duration,
222
+ easing: Easing.inOut(Easing.quad)
223
+ }), -1, true));
224
+ return () => cancelAnimation(progress);
225
+ }, [animate, delay, duration, from, to, still, progress]);
226
+ const style = useAnimatedStyle(() => mode === 'height' ? {
227
+ height: progress.value
228
+ } : {
229
+ transform: [{
230
+ scaleY: progress.value
231
+ }]
232
+ });
233
+ return /*#__PURE__*/_jsx(Animated.View, {
234
+ style: [{
235
+ width,
236
+ borderRadius: width / 2,
237
+ backgroundColor: color,
238
+ ...(mode === 'scale' ? {
239
+ height: barHeight
240
+ } : null)
241
+ }, style]
242
+ });
243
+ });
244
+
245
+ /* -------------------------------------------------------------------------- */
246
+ /* liquid dots */
247
+ /* -------------------------------------------------------------------------- */
248
+
249
+ /**
250
+ * Two blobs that merge as they cross.
251
+ *
252
+ * The merge is the whole animation, and it is drawn rather than filtered: a
253
+ * blur-and-threshold pass is how this is usually done, and SVG filters do not
254
+ * render on native at all. Instead the two circles and the neck between them
255
+ * are three subpaths of one path, unioned by the non-zero fill rule — which
256
+ * costs one animated prop a frame, and gives a crisper edge than a threshold
257
+ * ever does.
258
+ *
259
+ * The neck is what sells it. It pinches inward as the blobs separate and
260
+ * vanishes once they are far enough apart, so they let go of each other
261
+ * instead of snapping.
262
+ */
263
+ function LiquidDots({
264
+ color,
265
+ scale,
266
+ speed,
267
+ animate
268
+ }) {
269
+ const g = GEOMETRY['liquid-dots'];
270
+ const r = g.radius * scale;
271
+ const travel = g.travel * scale;
272
+ const width = g.width * scale;
273
+ const height = g.height * scale;
274
+ const cy = height / 2;
275
+ const mid = width / 2;
276
+
277
+ // -1 to 1: where the left blob is along its travel. The right blob mirrors.
278
+ const t = useSharedValue(animate ? -1 : -0.35);
279
+ useEffect(() => {
280
+ if (!animate) {
281
+ t.value = -0.35;
282
+ return;
283
+ }
284
+ t.value = -1;
285
+ t.value = withRepeat(withTiming(1, {
286
+ duration: 2000 / speed,
287
+ easing: Easing.inOut(Easing.quad)
288
+ }), -1, true);
289
+ return () => cancelAnimation(t);
290
+ }, [animate, speed, t]);
291
+ const props = useAnimatedProps(() => {
292
+ const x1 = mid + t.value * travel;
293
+ const x2 = mid - t.value * travel;
294
+ const left = Math.min(x1, x2);
295
+ const right = Math.max(x1, x2);
296
+ const gap = right - left;
297
+ let d = circle(left, cy, r) + circle(right, cy, r);
298
+
299
+ /*
300
+ * The neck only exists while the blobs are close enough to be pulling on
301
+ * each other: `1` when they are concentric, and gone by the time they are
302
+ * a radius apart. Squared, so it thins out quickly rather than trailing.
303
+ *
304
+ * The thickness is what the cutoff is really on. A falloff that only
305
+ * approaches zero leaves a hairline spanning the gap for the whole of the
306
+ * rest of the travel, and a one-pixel bar between two circles reads as a
307
+ * drawing mistake rather than as a thinning neck — so below the point
308
+ * where it is still legibly a neck, it is not drawn at all.
309
+ */
310
+ const pull = Math.max(0, 1 - gap / (r * 2.2));
311
+ const half = r * pull * pull * 0.95;
312
+ if (half > r * 0.12 && gap > 1) {
313
+ // Pinched at the middle: the control point sits inside the straight
314
+ // line between the two, which is what makes the join read as surface
315
+ // tension rather than as a rectangle.
316
+ const waist = half * 0.55;
317
+ d += `M${q(left)},${q(cy - half)}` + `Q${q((left + right) / 2)},${q(cy - waist)} ${q(right)},${q(cy - half)}` + `L${q(right)},${q(cy + half)}` + `Q${q((left + right) / 2)},${q(cy + waist)} ${q(left)},${q(cy + half)}Z`;
318
+ }
319
+ return {
320
+ d
321
+ };
322
+ });
323
+ return /*#__PURE__*/_jsx(Svg, {
324
+ width: width,
325
+ height: height,
326
+ children: /*#__PURE__*/_jsx(AnimatedPath, {
327
+ animatedProps: props,
328
+ fill: color,
329
+ fillRule: "nonzero"
330
+ })
331
+ });
332
+ }
333
+
334
+ /* -------------------------------------------------------------------------- */
335
+ /* wave physics */
336
+ /* -------------------------------------------------------------------------- */
337
+
338
+ /** How many bounces the ball takes crossing the row once. */
339
+ const BOUNCES = 4;
340
+
341
+ /**
342
+ * A ball bouncing along a row of bars, pushing them down where it lands.
343
+ *
344
+ * The only variant with anything like physics in it, and the reason it is
345
+ * procedural rather than a keyframe list: the bar heights are a function of
346
+ * where the ball is, so they are derived every frame instead of stored. Fifteen
347
+ * bars precomputed would be fifteen arrays that have to agree with each other
348
+ * and with the ball, and none of them could respond to `speed`.
349
+ *
350
+ * Both the bars and the ball read the same clock, which is what keeps the
351
+ * dent under the ball rather than near it.
352
+ */
353
+ function WavePhysics({
354
+ color,
355
+ scale,
356
+ speed,
357
+ animate
358
+ }) {
359
+ const g = GEOMETRY['wave-physics'];
360
+ const barWidth = g.bar * scale;
361
+ const step = (g.bar + g.gap) * scale;
362
+ const base = g.base * scale;
363
+ const peak = g.peak * scale;
364
+ const ballR = g.bar * 1.1 * scale;
365
+ const bounce = peak * 0.9;
366
+ const width = (g.count - 1) * step + barWidth;
367
+ const height = base + peak + bounce + ballR * 2;
368
+ const floor = height;
369
+ const clock = useSharedValue(0);
370
+ const frame = useFrameCallback(info => {
371
+ 'worklet';
372
+
373
+ // Accumulated rather than read off the total, so `speed` can change
374
+ // mid-animation without the ball jumping to where the new rate would have
375
+ // put it. A dropped frame is clamped: a 300ms hitch played at full rate
376
+ // is a lurch, not a bounce.
377
+ const delta = Math.min(info.timeSincePreviousFrame ?? 16, 48) / 1000;
378
+ clock.value += delta * speed * 0.35;
379
+ }, false);
380
+ const {
381
+ setActive
382
+ } = frame;
383
+ useEffect(() => {
384
+ setActive(animate);
385
+ return () => setActive(false);
386
+ }, [animate, setActive]);
387
+
388
+ /** Where the ball is, as a bar index, and how far through its hop. */
389
+ const ballAt = time => {
390
+ 'worklet';
391
+
392
+ // Back and forth along the row, then back again — one continuous pass.
393
+ const cycle = time % 2;
394
+ const across = cycle < 1 ? cycle : 2 - cycle;
395
+ const index = across * (g.count - 1);
396
+ let phase = across * BOUNCES % 1;
397
+ if (across === 0 || across === 1) phase = 0;
398
+ // A parabola per hop: the ball is slowest at the top, which is the only
399
+ // part of a bounce anyone actually reads.
400
+ const lift = 4 * phase * (1 - phase);
401
+ return {
402
+ index,
403
+ lift
404
+ };
405
+ };
406
+ const barProps = useAnimatedProps(() => {
407
+ const {
408
+ index,
409
+ lift
410
+ } = animate ? ballAt(clock.value) : {
411
+ index: 7,
412
+ lift: 0.5
413
+ };
414
+ // The ball presses hardest when it is on the ground, so the dent is the
415
+ // inverse of the hop.
416
+ const press = Math.max(0, 1 - lift * 2);
417
+ let d = '';
418
+ for (let i = 0; i < g.count; i++) {
419
+ const distance = Math.abs(i - index);
420
+ // A raised crest travelling with the ball, three bars wide either side.
421
+ const crest = distance < 3 ? Math.cos(distance / 3 * (Math.PI / 2)) : 0;
422
+ // And a dent right under it, narrower than the crest so the bar the
423
+ // ball is standing on is pushed down through its own rise.
424
+ const dent = distance < 1.5 ? Math.cos(distance / 1.5 * (Math.PI / 2)) * press * (peak * 0.5) : 0;
425
+ const barHeight = Math.max(base * 0.5, base + crest * peak - dent);
426
+ const x = q(barWidth / 2 + i * step);
427
+ d += `M${x},${q(floor)}L${x},${q(floor - barHeight)}`;
428
+ }
429
+ return {
430
+ d
431
+ };
432
+ });
433
+ const ballProps = useAnimatedProps(() => {
434
+ const {
435
+ index,
436
+ lift
437
+ } = animate ? ballAt(clock.value) : {
438
+ index: 7,
439
+ lift: 0.5
440
+ };
441
+ const press = Math.max(0, 1 - lift * 2);
442
+ // The ball rides the bar directly under it, which is the crest of the
443
+ // wave (so the full `peak`) minus however hard it is pressing.
444
+ const rest = base + peak - press * (peak * 0.5);
445
+
446
+ // Squash on the ground, stretch at the top — the oldest trick there is,
447
+ // and the thing that separates a bouncing ball from a moving circle.
448
+ const squash = press;
449
+ return {
450
+ cx: q(barWidth / 2 + index * step),
451
+ cy: q(floor - rest - ballR + squash * ballR * 0.3 - lift * bounce),
452
+ rx: q(ballR * (1 + squash * 0.25)),
453
+ ry: q(ballR * (1 - squash * 0.28))
454
+ };
455
+ });
456
+ return /*#__PURE__*/_jsxs(Svg, {
457
+ width: width,
458
+ height: height,
459
+ children: [/*#__PURE__*/_jsx(AnimatedPath, {
460
+ animatedProps: barProps,
461
+ stroke: color,
462
+ strokeWidth: barWidth,
463
+ strokeLinecap: "round",
464
+ fill: "none",
465
+ opacity: 0.55
466
+ }), /*#__PURE__*/_jsx(AnimatedEllipse, {
467
+ animatedProps: ballProps,
468
+ fill: color
469
+ })]
470
+ });
471
+ }
472
+
473
+ /* -------------------------------------------------------------------------- */
474
+ /* morph ring */
475
+ /* -------------------------------------------------------------------------- */
476
+
477
+ /**
478
+ * A square turning into a circle and back while it rotates.
479
+ *
480
+ * The rotation is what makes the morph legible: a square becoming a circle in
481
+ * place mostly reads as a square getting smaller, and turning it means the
482
+ * corners are visibly going somewhere as they round off.
483
+ */
484
+ function MorphRing({
485
+ color,
486
+ scale,
487
+ speed,
488
+ animate
489
+ }) {
490
+ const g = GEOMETRY['morph-ring'];
491
+ const box = g.box * scale;
492
+ const border = Math.max(2, g.border * scale);
493
+ const progress = useSharedValue(animate ? 0 : 0.5);
494
+ useEffect(() => {
495
+ if (!animate) {
496
+ progress.value = 0.5;
497
+ return;
498
+ }
499
+ progress.value = 0;
500
+ progress.value = withRepeat(withTiming(1, {
501
+ duration: 2000 / speed,
502
+ easing: Easing.inOut(Easing.quad)
503
+ }), -1, true);
504
+ return () => cancelAnimation(progress);
505
+ }, [animate, speed, progress]);
506
+ const style = useAnimatedStyle(() => ({
507
+ borderRadius: box * (0.1 + progress.value * 0.4),
508
+ transform: [{
509
+ rotate: `${progress.value * 180}deg`
510
+ }]
511
+ }));
512
+
513
+ /*
514
+ * The padding is the rotation's. A square turned 45° needs its diagonal, so
515
+ * it sweeps past its own box by nearly half its width on each side — laid
516
+ * out flush, the corners spill over whatever is next to it.
517
+ */
518
+ const overhang = (box * Math.SQRT2 - box) / 2;
519
+ return /*#__PURE__*/_jsx(View, {
520
+ style: {
521
+ padding: overhang
522
+ },
523
+ children: /*#__PURE__*/_jsx(Animated.View, {
524
+ style: [{
525
+ width: box,
526
+ height: box,
527
+ borderWidth: border,
528
+ borderColor: color
529
+ }, style]
530
+ })
531
+ });
532
+ }
533
+
534
+ /* -------------------------------------------------------------------------- */
535
+ /* root */
536
+ /* -------------------------------------------------------------------------- */
537
+
538
+ /**
539
+ * A loading animation.
540
+ *
541
+ * Nine of them, chosen with `variant`. They all take the same colour, the same
542
+ * `speed`, and the same treatment under reduced motion, so swapping one for
543
+ * another is a one-word change.
544
+ */
545
+ export const Loader = /*#__PURE__*/memo(function Loader({
546
+ variant = 'pulse-dots',
547
+ size = 'md',
548
+ color,
549
+ speed = 1,
550
+ label = 'Loading',
551
+ className,
552
+ ...props
553
+ }) {
554
+ const reducedMotion = useReducedMotion();
555
+ const animate = !reducedMotion && speed > 0;
556
+ const scale = SIZE_SCALE[size];
557
+ const rate = speed > 0 ? speed : 1;
558
+
559
+ /*
560
+ * A loader inside a filled surface has to be drawn in that surface's
561
+ * foreground rather than the page's: a button in the primary colour is one
562
+ * of the most likely places for a loader to appear, and in most themes the
563
+ * primary colour *is* the page's foreground — so one resolving
564
+ * `--color-foreground` for itself would be invisible exactly there.
565
+ * Surfaces already publish their readable foreground for icons.
566
+ */
567
+ const inherited = useIconColor();
568
+ const token = useCSSVariable(isToken(color) ? color : '--color-foreground');
569
+ const resolved = typeof token === 'string' ? token : undefined;
570
+ const ink = (isToken(color) ? resolved : color) ?? inherited ?? resolved ?? '#0a0a0a';
571
+ const frame = content => /*#__PURE__*/_jsx(View, {
572
+ accessibilityRole: "progressbar",
573
+ accessibilityLabel: label,
574
+ className: loaderVariants({
575
+ className
576
+ }),
577
+ ...props,
578
+ children: content
579
+ });
580
+ if (variant === 'liquid-dots') {
581
+ return frame(/*#__PURE__*/_jsx(LiquidDots, {
582
+ color: ink,
583
+ scale: scale,
584
+ speed: rate,
585
+ animate: animate
586
+ }));
587
+ }
588
+ if (variant === 'wave-physics') {
589
+ return frame(/*#__PURE__*/_jsx(WavePhysics, {
590
+ color: ink,
591
+ scale: scale,
592
+ speed: rate,
593
+ animate: animate
594
+ }));
595
+ }
596
+ if (variant === 'morph-ring') {
597
+ return frame(/*#__PURE__*/_jsx(MorphRing, {
598
+ color: ink,
599
+ scale: scale,
600
+ speed: rate,
601
+ animate: animate
602
+ }));
603
+ }
604
+ if (variant === 'pulse-dots') {
605
+ const g = GEOMETRY['pulse-dots'];
606
+ return frame(/*#__PURE__*/_jsx(View, {
607
+ className: "flex-row items-center",
608
+ style: {
609
+ gap: g.gap * scale
610
+ },
611
+ children: Array.from({
612
+ length: g.count
613
+ }, (_unused, i) => /*#__PURE__*/_jsx(Dot, {
614
+ size: g.dot * scale,
615
+ color: ink,
616
+ delay: i * 200 / rate,
617
+ duration: 700 / rate,
618
+ from: 0.2,
619
+ to: 1,
620
+ still: i === 1 ? 1 : 0.45,
621
+ property: "opacity",
622
+ animate: animate
623
+ }, i))
624
+ }));
625
+ }
626
+ if (variant === 'bounce-dots') {
627
+ const g = GEOMETRY['bounce-dots'];
628
+ const travel = g.travel * scale;
629
+ return frame(/*#__PURE__*/_jsx(View, {
630
+ className: "flex-row items-center",
631
+ style: {
632
+ gap: g.gap * scale,
633
+ paddingTop: travel
634
+ },
635
+ children: Array.from({
636
+ length: g.count
637
+ }, (_unused, i) => /*#__PURE__*/_jsx(Dot, {
638
+ size: g.dot * scale,
639
+ color: ink,
640
+ delay: i * 100 / rate,
641
+ duration: 300 / rate,
642
+ from: 0,
643
+ to: -travel,
644
+ still: -travel * (i === 1 ? 1 : 0.35),
645
+ property: "translateY",
646
+ animate: animate
647
+ }, i))
648
+ }));
649
+ }
650
+ if (variant === 'pulsating-dots') {
651
+ const g = GEOMETRY['pulsating-dots'];
652
+ return frame(/*#__PURE__*/_jsx(View, {
653
+ className: "flex-row items-center"
654
+ // A dot at its largest is half again its laid-out size, and it grows
655
+ // about its centre — so a quarter of a dot on each side is the room
656
+ // the scale needs, plus a little for the rounding.
657
+ ,
658
+ style: {
659
+ gap: g.gap * scale,
660
+ padding: g.dot * scale * 0.3
661
+ },
662
+ children: Array.from({
663
+ length: g.count
664
+ }, (_unused, i) => /*#__PURE__*/_jsx(Dot, {
665
+ size: g.dot * scale,
666
+ color: ink,
667
+ delay: i * 200 / rate,
668
+ duration: 600 / rate,
669
+ from: 1,
670
+ to: 1.5,
671
+ still: i === 1 ? 1.4 : 1.1,
672
+ property: "scale",
673
+ animate: animate
674
+ }, i))
675
+ }));
676
+ }
677
+ if (variant === 'bar-cascade') {
678
+ const g = GEOMETRY['bar-cascade'];
679
+ return frame(/*#__PURE__*/_jsx(View, {
680
+ className: "flex-row items-center",
681
+ style: {
682
+ gap: g.gap * scale,
683
+ height: g.max * scale
684
+ },
685
+ children: Array.from({
686
+ length: g.count
687
+ }, (_unused, i) => /*#__PURE__*/_jsx(Bar, {
688
+ width: g.bar * scale,
689
+ color: ink,
690
+ delay: i * 100 / rate,
691
+ duration: 500 / rate,
692
+ from: g.min * scale,
693
+ to: g.max * scale
694
+ // A still frame that shows the cascade's shape rather than a row
695
+ // of equal bars, which would say nothing about what it does.
696
+ ,
697
+ still: (g.min + (g.max - g.min) * Math.abs(Math.sin(i * 0.9))) * scale,
698
+ mode: "height",
699
+ animate: animate
700
+ }, i))
701
+ }));
702
+ }
703
+ if (variant === 'bouncing-bars') {
704
+ const g = GEOMETRY['bouncing-bars'];
705
+ return frame(/*#__PURE__*/_jsx(View, {
706
+ className: "flex-row items-center",
707
+ style: {
708
+ gap: g.gap * scale,
709
+ height: g.height * scale
710
+ },
711
+ children: Array.from({
712
+ length: g.count
713
+ }, (_unused, i) => /*#__PURE__*/_jsx(Bar, {
714
+ width: g.bar * scale,
715
+ color: ink,
716
+ delay: i * 200 / rate,
717
+ duration: 500 / rate,
718
+ from: 0.3,
719
+ to: 1,
720
+ barHeight: g.height * scale,
721
+ still: 0.4 + i * 0.25,
722
+ mode: "scale",
723
+ animate: animate
724
+ }, i))
725
+ }));
726
+ }
727
+
728
+ // symmetric-wave
729
+ const g = GEOMETRY['symmetric-wave'];
730
+ /*
731
+ * The delay is symmetric about the middle rather than running left to right,
732
+ * so the wave opens outward from the centre and arrives back at it. A
733
+ * straight ramp reads as a queue; this reads as one thing breathing.
734
+ */
735
+ const offsets = [0, 1, 2, 3, 4, 3, 2, 1, 0];
736
+ return frame(/*#__PURE__*/_jsx(View, {
737
+ className: "flex-row items-center",
738
+ style: {
739
+ gap: g.gap * scale,
740
+ height: g.max * scale
741
+ },
742
+ children: Array.from({
743
+ length: g.count
744
+ }, (_unused, i) => /*#__PURE__*/_jsx(Bar, {
745
+ width: g.bar * scale,
746
+ color: ink,
747
+ delay: (offsets[i] ?? 0) * 100 / rate,
748
+ duration: 600 / rate,
749
+ from: g.min * scale,
750
+ to: g.max * scale,
751
+ still: (g.min + (g.max - g.min) * (1 - (offsets[i] ?? 0) / 4)) * scale,
752
+ mode: "height",
753
+ animate: animate
754
+ }, i))
755
+ }));
756
+ });
757
+ Loader.displayName = 'Loader';
758
+ //# sourceMappingURL=index.js.map