panelui-native 0.12.0 → 0.14.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 (93) hide show
  1. package/README.md +39 -23
  2. package/lib/module/components/button/index.js +12 -9
  3. package/lib/module/components/button/index.js.map +1 -1
  4. package/lib/module/components/chip/index.js +313 -0
  5. package/lib/module/components/chip/index.js.map +1 -0
  6. package/lib/module/components/frame/index.js +41 -39
  7. package/lib/module/components/frame/index.js.map +1 -1
  8. package/lib/module/components/heatmap-chart/index.js +772 -0
  9. package/lib/module/components/heatmap-chart/index.js.map +1 -0
  10. package/lib/module/components/input/index.js +4 -0
  11. package/lib/module/components/input/index.js.map +1 -1
  12. package/lib/module/components/scroll-canvas/index.js +175 -0
  13. package/lib/module/components/scroll-canvas/index.js.map +1 -0
  14. package/lib/module/components/scroll-text/index.js +213 -0
  15. package/lib/module/components/scroll-text/index.js.map +1 -0
  16. package/lib/module/components/select/index.js +20 -24
  17. package/lib/module/components/select/index.js.map +1 -1
  18. package/lib/module/components/slider/index.js +2 -5
  19. package/lib/module/components/slider/index.js.map +1 -1
  20. package/lib/module/components/switch/index.js +15 -17
  21. package/lib/module/components/switch/index.js.map +1 -1
  22. package/lib/module/components/thinking-orb/index.js +741 -0
  23. package/lib/module/components/thinking-orb/index.js.map +1 -0
  24. package/lib/module/components/toggle-button/index.js +12 -3
  25. package/lib/module/components/toggle-button/index.js.map +1 -1
  26. package/lib/module/hooks/index.js +1 -0
  27. package/lib/module/hooks/index.js.map +1 -1
  28. package/lib/module/hooks/use-keyboard-avoidance.js +99 -59
  29. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  30. package/lib/module/hooks/use-reveal-progress.js +75 -0
  31. package/lib/module/hooks/use-reveal-progress.js.map +1 -0
  32. package/lib/module/index.js +6 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/native/index.js.map +1 -1
  35. package/lib/module/primitives/keyboard-avoider.js +21 -2
  36. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  37. package/lib/module/primitives/scroll-progress.js +114 -0
  38. package/lib/module/primitives/scroll-progress.js.map +1 -0
  39. package/lib/typescript/src/components/button/index.d.ts +3 -4
  40. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/chip/index.d.ts +304 -0
  42. package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
  43. package/lib/typescript/src/components/frame/index.d.ts +15 -13
  44. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  45. package/lib/typescript/src/components/heatmap-chart/index.d.ts +260 -0
  46. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -0
  47. package/lib/typescript/src/components/input/index.d.ts +19 -4
  48. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  49. package/lib/typescript/src/components/scroll-canvas/index.d.ts +66 -0
  50. package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -0
  51. package/lib/typescript/src/components/scroll-text/index.d.ts +46 -0
  52. package/lib/typescript/src/components/scroll-text/index.d.ts.map +1 -0
  53. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  55. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  56. package/lib/typescript/src/components/thinking-orb/index.d.ts +28 -0
  57. package/lib/typescript/src/components/thinking-orb/index.d.ts.map +1 -0
  58. package/lib/typescript/src/components/toggle-button/index.d.ts +12 -0
  59. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
  60. package/lib/typescript/src/hooks/index.d.ts +2 -1
  61. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  62. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +16 -3
  63. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  64. package/lib/typescript/src/hooks/use-reveal-progress.d.ts +56 -0
  65. package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -0
  66. package/lib/typescript/src/index.d.ts +6 -0
  67. package/lib/typescript/src/index.d.ts.map +1 -1
  68. package/lib/typescript/src/native/index.d.ts +15 -7
  69. package/lib/typescript/src/native/index.d.ts.map +1 -1
  70. package/lib/typescript/src/primitives/keyboard-avoider.d.ts +30 -3
  71. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  72. package/lib/typescript/src/primitives/scroll-progress.d.ts +61 -0
  73. package/lib/typescript/src/primitives/scroll-progress.d.ts.map +1 -0
  74. package/package.json +1 -1
  75. package/src/components/button/index.tsx +13 -11
  76. package/src/components/chip/index.tsx +333 -0
  77. package/src/components/frame/index.tsx +39 -50
  78. package/src/components/heatmap-chart/index.tsx +1024 -0
  79. package/src/components/input/index.tsx +23 -4
  80. package/src/components/scroll-canvas/index.tsx +220 -0
  81. package/src/components/scroll-text/index.tsx +277 -0
  82. package/src/components/select/index.tsx +4 -9
  83. package/src/components/slider/index.tsx +4 -7
  84. package/src/components/switch/index.tsx +5 -8
  85. package/src/components/thinking-orb/index.tsx +792 -0
  86. package/src/components/toggle-button/index.tsx +24 -2
  87. package/src/hooks/index.ts +6 -0
  88. package/src/hooks/use-keyboard-avoidance.ts +111 -65
  89. package/src/hooks/use-reveal-progress.ts +106 -0
  90. package/src/index.ts +46 -0
  91. package/src/native/index.ts +15 -7
  92. package/src/primitives/keyboard-avoider.tsx +41 -4
  93. package/src/primitives/scroll-progress.tsx +167 -0
@@ -0,0 +1,741 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * ThinkingOrb — a dotted orb that says what an agent is doing.
5
+ *
6
+ * A spinner says "busy". These say *which kind of busy*: particles running
7
+ * tilted orbits for work in flight, a scan meridian sweeping a globe for a
8
+ * search, bands that scramble and click back for a solve. Six states, each a
9
+ * distinct silhouette in motion, so a glance at the orb is enough — which is
10
+ * the whole point of putting one next to a streaming reply.
11
+ *
12
+ * ```tsx
13
+ * <ThinkingOrb state="searching" />
14
+ * <ThinkingOrb state="working" size={20} />
15
+ * ```
16
+ *
17
+ * ## How it is drawn
18
+ *
19
+ * The geometry is honestly three-dimensional — points on a sphere, rotated and
20
+ * tilted, projected orthographically, with depth carried by dot size and ink
21
+ * weight. React Native has no 2D canvas to paint that into, and one animated
22
+ * SVG node per dot would be two hundred native prop writes a frame, which no
23
+ * amount of tuning survives.
24
+ *
25
+ * So the dots are quantised into eight ink buckets and each bucket is emitted
26
+ * as a *single* path of circle arcs. Eight animated props a frame, whatever the
27
+ * dot count, and depth ordering falls out of bucket order for free — depth is
28
+ * what drives the ink in the first place, so painting faint to strong paints
29
+ * far to near. Everything from the trigonometry to the path strings runs in one
30
+ * worklet on the UI thread; React renders once and then never again.
31
+ *
32
+ * Strictly monochrome, from `--color-foreground`, so the orb inverts with the
33
+ * theme and needs no palette of its own.
34
+ */
35
+ import { useEffect } from 'react';
36
+ import { View } from 'react-native';
37
+ import Animated, { runOnUI, useAnimatedProps, useFrameCallback, useReducedMotion, useSharedValue } from 'react-native-reanimated';
38
+ import Svg, { Path } from 'react-native-svg';
39
+ import { useCSSVariable } from 'uniwind';
40
+ import { cn } from "../../utils/cn.js";
41
+ import { jsx as _jsx } from "react/jsx-runtime";
42
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
43
+
44
+ /**
45
+ * How many ink levels the dots are rounded to.
46
+ *
47
+ * This is the dial. Every bucket is one animated prop and one path per frame,
48
+ * so fewer is cheaper and more is smoother; below about six the depth falloff
49
+ * starts to band visibly, and above about twelve there is nothing left to see.
50
+ */
51
+ const BUCKETS = 8;
52
+
53
+ /** Ink below this is invisible against any background, so it is not drawn. */
54
+ const MIN_INK = 0.03;
55
+
56
+ /** Dots never shrink past this, or a far dot becomes a gap in the lattice. */
57
+ const MIN_RADIUS = 0.3;
58
+
59
+ /** The frame a still orb shows. Far enough in that no state is at its start. */
60
+ const STILL_FRAME = 2.4;
61
+ /** What each state is doing, for anyone who cannot see it. */
62
+ const STATE_LABEL = {
63
+ working: 'Working',
64
+ searching: 'Searching',
65
+ solving: 'Solving',
66
+ listening: 'Listening',
67
+ composing: 'Composing',
68
+ shaping: 'Shaping'
69
+ };
70
+
71
+ /**
72
+ * Density and speed per state, at the two scales.
73
+ *
74
+ * `sm` is not `md` scaled down. Below about thirty pixels a faithful lattice
75
+ * turns to grey mush, so the small orb is a separate design: far fewer dots,
76
+ * each proportionally much larger, running faster so the motion still reads at
77
+ * a size where the individual dots barely do.
78
+ *
79
+ * The counts are lower than a canvas implementation would use, because every
80
+ * dot here is arc commands in a path string built each frame — the cost is in
81
+ * the string, not in the fill.
82
+ */
83
+
84
+ const PROFILES = {
85
+ working: {
86
+ md: {
87
+ speed: 1.9,
88
+ rows: 9,
89
+ density: 26,
90
+ radius: 1
91
+ },
92
+ sm: {
93
+ speed: 3.9,
94
+ rows: 4,
95
+ density: 12,
96
+ radius: 2.4
97
+ }
98
+ },
99
+ searching: {
100
+ md: {
101
+ speed: 2.0,
102
+ rows: 11,
103
+ density: 28,
104
+ radius: 1.15
105
+ },
106
+ sm: {
107
+ speed: 2.7,
108
+ rows: 5,
109
+ density: 12,
110
+ radius: 1.75
111
+ }
112
+ },
113
+ solving: {
114
+ md: {
115
+ speed: 1.8,
116
+ rows: 10,
117
+ density: 24,
118
+ radius: 1.05
119
+ },
120
+ sm: {
121
+ speed: 2.0,
122
+ rows: 5,
123
+ density: 11,
124
+ radius: 1.9
125
+ }
126
+ },
127
+ listening: {
128
+ md: {
129
+ speed: 4.4,
130
+ rows: 10,
131
+ density: 24,
132
+ radius: 1
133
+ },
134
+ sm: {
135
+ speed: 4.0,
136
+ rows: 5,
137
+ density: 11,
138
+ radius: 1.6
139
+ }
140
+ },
141
+ composing: {
142
+ md: {
143
+ speed: 2.3,
144
+ rows: 5,
145
+ density: 44,
146
+ radius: 0.85
147
+ },
148
+ sm: {
149
+ speed: 3.1,
150
+ rows: 3,
151
+ density: 20,
152
+ radius: 1.1
153
+ }
154
+ },
155
+ shaping: {
156
+ md: {
157
+ speed: 2.4,
158
+ rows: 1,
159
+ density: 26,
160
+ radius: 1
161
+ },
162
+ sm: {
163
+ speed: 2.1,
164
+ rows: 1,
165
+ density: 14,
166
+ radius: 2.2
167
+ }
168
+ }
169
+ };
170
+
171
+ /* -------------------------------------------------------------------------- */
172
+ /* Worklet maths */
173
+ /* -------------------------------------------------------------------------- */
174
+
175
+ /** Deterministic hash in `[0, 1)`. Stable across frames and across mounts. */
176
+ function hashD(a, b) {
177
+ 'worklet';
178
+
179
+ const h = Math.sin(a * 12.9898 + b * 78.233) * 43758.5453;
180
+ return h - Math.floor(h);
181
+ }
182
+
183
+ /** Shortest signed angular distance, wrapped to `(-π, π]`. */
184
+ function angleDelta(a, b) {
185
+ 'worklet';
186
+
187
+ return Math.atan2(Math.sin(a - b), Math.cos(a - b));
188
+ }
189
+
190
+ /** One decimal place. Path strings are rebuilt every frame; every digit costs. */
191
+ function q(value) {
192
+ 'worklet';
193
+
194
+ return Math.round(value * 10) / 10;
195
+ }
196
+
197
+ /**
198
+ * Dot radii were tuned against a 300pt frame; scaling them sub-linearly is what
199
+ * keeps a 20pt orb from becoming a smudge and a 96pt one from becoming beads.
200
+ */
201
+ function radiusScale(size) {
202
+ 'worklet';
203
+
204
+ return (size / 300) ** 0.6;
205
+ }
206
+
207
+ /**
208
+ * A dot, straight into its ink bucket as a circle of arcs.
209
+ *
210
+ * Two half-arcs rather than a `<Circle>`, because a bucket has to be one node:
211
+ * the entire point of bucketing is that the frame costs eight prop writes and
212
+ * not two hundred.
213
+ */
214
+ function dot(out, x, y, r, ink) {
215
+ 'worklet';
216
+
217
+ if (ink < MIN_INK) return;
218
+ const bucket = Math.min(BUCKETS - 1, Math.max(0, Math.floor(ink * BUCKETS)));
219
+ const rr = q(Math.max(MIN_RADIUS, r));
220
+ const d = q(rr * 2);
221
+ out[bucket] += `M${q(x - rr)} ${q(y)}a${rr} ${rr} 0 1 0 ${d} 0a${rr} ${rr} 0 1 0 ${-d} 0`;
222
+ }
223
+
224
+ /**
225
+ * Spin, tilt and orthographic projection, written into a scratch triple.
226
+ *
227
+ * It writes into an array the caller owns rather than returning one, because it
228
+ * is called once per dot per frame — a fresh triple each time is a few hundred
229
+ * allocations a frame for a value read immediately and thrown away.
230
+ */
231
+ function project(out, x, y, z, sy, cy, st, ct, ox, oy, scale) {
232
+ 'worklet';
233
+
234
+ const x1 = x * cy + z * sy;
235
+ const z1 = -x * sy + z * cy;
236
+ const y1 = y * ct - z1 * st;
237
+ out[0] = ox + x1 * scale;
238
+ out[1] = oy - y1 * scale;
239
+ out[2] = y * st + z1 * ct;
240
+ }
241
+
242
+ /* -------------------------------------------------------------------------- */
243
+ /* The six states */
244
+ /* -------------------------------------------------------------------------- */
245
+
246
+ /**
247
+ * `working` — particles running tilted orbits, each on a faint ghost path.
248
+ *
249
+ * The ghosts are what make it read as orbits rather than as loose specks: three
250
+ * bright particles alone look random, and the same three on a visible track
251
+ * look like work being done.
252
+ */
253
+ function drawWorking(out, size, t, p) {
254
+ 'worklet';
255
+
256
+ const c = size / 2;
257
+ const R = c * 0.82;
258
+ const rs = radiusScale(size) * p.radius;
259
+ const sy = Math.sin(t * 0.12);
260
+ const cy = Math.cos(t * 0.12);
261
+ const st = Math.sin(0.3);
262
+ const ct = Math.cos(0.3);
263
+ const v = [0, 0, 0];
264
+ for (let orbit = 0; orbit < p.rows; orbit++) {
265
+ const h1 = hashD(orbit, 1.7);
266
+ const h2 = hashD(orbit, 5.2);
267
+ const h3 = hashD(orbit, 8.9);
268
+ const ro = R * (0.45 + 0.52 * h1);
269
+ const theta = h1 * 2 * Math.PI;
270
+ const phi = Math.acos(2 * h2 - 1);
271
+
272
+ // An orthonormal basis for the orbit's plane, from its normal.
273
+ const nx = Math.sin(phi) * Math.cos(theta);
274
+ const ny = Math.cos(phi);
275
+ const nz = Math.sin(phi) * Math.sin(theta);
276
+ const ul = Math.max(1e-6, Math.hypot(ny, nx));
277
+ const ux = -ny / ul;
278
+ const uy = nx / ul;
279
+ const vx = ny * 0 - nz * uy;
280
+ const vy = nz * ux - nx * 0;
281
+ const vz = nx * uy - ny * ux;
282
+ const speed = (0.25 + 0.55 * h3) * (h3 > 0.5 ? 1 : -1);
283
+ for (let k = 0; k < p.density; k++) {
284
+ const a = k / p.density * 2 * Math.PI;
285
+ const ca = Math.cos(a);
286
+ const sa = Math.sin(a);
287
+ project(v, (ux * ca + vx * sa) * ro, (uy * ca + vy * sa) * ro, vz * sa * ro, sy, cy, st, ct, c, c, 1);
288
+ const depth = (v[2] / ro + 1) / 2;
289
+ dot(out, v[0], v[1], 0.9 * rs, 0.28 * (0.4 + 0.6 * depth));
290
+ }
291
+ for (let m = 0; m < 3; m++) {
292
+ const a = t * speed + m / 3 * 2 * Math.PI + h2 * 6;
293
+ const ca = Math.cos(a);
294
+ const sa = Math.sin(a);
295
+ project(v, (ux * ca + vx * sa) * ro, (uy * ca + vy * sa) * ro, vz * sa * ro, sy, cy, st, ct, c, c, 1);
296
+ const depth = (v[2] / ro + 1) / 2;
297
+ dot(out, v[0], v[1], (1.2 + 1.6 * depth) * rs, 0.7 + 0.22 * depth);
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * `searching` — a scan meridian sweeping a dotted globe.
304
+ *
305
+ * The scan is a ripple in dot *size*, not a highlight. A brightness sweep on a
306
+ * monochrome orb reads as a lighting effect; a size ripple reads as something
307
+ * passing over the surface, which is what a search is.
308
+ */
309
+ function drawSearching(out, size, t, p) {
310
+ 'worklet';
311
+
312
+ const c = size / 2;
313
+ const R = c * 0.82;
314
+ const rs = radiusScale(size) * p.radius;
315
+ const spin = 0.5;
316
+ const tilt = 0.4 + 0.06 * Math.sin(t * 0.35);
317
+ const sy = Math.sin(t * spin);
318
+ const cySpin = Math.cos(t * spin);
319
+ const st = Math.sin(tilt);
320
+ const ct = Math.cos(tilt);
321
+ const scan = t * (spin + (1.7 - spin) * 4.1);
322
+ const v = [0, 0, 0];
323
+ for (let li = 0; li <= p.rows; li++) {
324
+ const lat = -Math.PI / 2 + li / p.rows * Math.PI;
325
+ const cosLat = Math.cos(lat);
326
+ const sinLat = Math.sin(lat);
327
+ const count = Math.max(1, Math.round(Math.abs(cosLat) * p.density));
328
+ for (let lj = 0; lj < count; lj++) {
329
+ const lon = lj / count * 2 * Math.PI;
330
+ project(v, cosLat * Math.cos(lon), sinLat, cosLat * Math.sin(lon), sy, cySpin, st, ct, c, c, R);
331
+ const depth = (v[2] + 1) / 2;
332
+ const delta = angleDelta(lon + t * spin, scan);
333
+ const boost = Math.exp(-(delta * delta) / 0.18) * Math.max(0, v[2]);
334
+ const ink = 0.38 + 0.54 * depth;
335
+ dot(out, v[0], v[1], (0.6 + 1.7 * depth + boost) * rs, ink * (0.45 + 0.55 * Math.min(1, boost)));
336
+ }
337
+ }
338
+ }
339
+
340
+ /**
341
+ * `solving` — bands twist in quarter turns, then replay in reverse and click
342
+ * back to solved.
343
+ *
344
+ * The palindrome is the whole trick. A scramble that never resolves reads as
345
+ * confusion; one that comes apart and goes back together reads as an answer
346
+ * being found, and it loops seamlessly because it ends where it began.
347
+ */
348
+ function drawSolving(out, size, t, p) {
349
+ 'worklet';
350
+
351
+ const c = size / 2;
352
+ const R = c * 0.82;
353
+ const rs = radiusScale(size) * p.radius;
354
+ const yaw = t * 0.55;
355
+ const tilt = 0.35 + 0.1 * Math.sin(t * 0.9);
356
+ const sy = Math.sin(yaw);
357
+ const cySpin = Math.cos(yaw);
358
+ const st = Math.sin(tilt);
359
+ const ct = Math.cos(tilt);
360
+ const v = [0, 0, 0];
361
+ const moves = 10;
362
+ const slot = 0.42;
363
+ const rest = 1.2;
364
+ const cycle = 2 * moves * slot + rest;
365
+ const tc = t % cycle;
366
+
367
+ // How far through each move we are: all the way for the ones already made,
368
+ // eased for the one under way, nothing for the ones still to come.
369
+ const amount = [];
370
+ let active = -1;
371
+ for (let i = 0; i < moves; i++) amount.push(0);
372
+ if (tc < 2 * moves * slot) {
373
+ const index = Math.floor(tc / slot);
374
+ const local = Math.min(1, (tc - index * slot) / slot / 0.7);
375
+ const eased = 1 - (1 - local) ** 3;
376
+ if (index < moves) {
377
+ for (let i = 0; i < index; i++) amount[i] = 1;
378
+ amount[index] = eased;
379
+ active = index;
380
+ } else {
381
+ const undo = 2 * moves - 1 - index;
382
+ for (let i = 0; i < undo; i++) amount[i] = 1;
383
+ amount[undo] = 1 - eased;
384
+ active = undo;
385
+ }
386
+ }
387
+ for (let li = 0; li <= p.rows; li++) {
388
+ const lat = -Math.PI / 2 + li / p.rows * Math.PI;
389
+ const cosLat = Math.cos(lat);
390
+ const sinLat = Math.sin(lat);
391
+ const count = Math.max(1, Math.round(Math.abs(cosLat) * p.density));
392
+ for (let lj = 0; lj < count; lj++) {
393
+ const lon = lj / count * 2 * Math.PI;
394
+ let x = cosLat * Math.cos(lon);
395
+ let y = sinLat;
396
+ let z = cosLat * Math.sin(lon);
397
+ let inActive = false;
398
+ for (let i = 0; i < moves; i++) {
399
+ if (amount[i] <= 0) continue;
400
+ const axis = Math.min(2, Math.floor(hashD(i, 2.3) * 3));
401
+ const lo = -1 + 0.5 * Math.min(3, Math.floor(hashD(i, 5.9) * 4));
402
+ const coord = axis === 0 ? x : axis === 1 ? y : z;
403
+ if (coord < lo || coord >= lo + 0.5) continue;
404
+ if (i === active) inActive = true;
405
+ const dir = hashD(i, 7.7) < 0.5 ? 1 : -1;
406
+ const a = dir * Math.PI / 2 * amount[i];
407
+ const ca = Math.cos(a);
408
+ const sa = Math.sin(a);
409
+ if (axis === 0) {
410
+ const y2 = y * ca - z * sa;
411
+ z = y * sa + z * ca;
412
+ y = y2;
413
+ } else if (axis === 1) {
414
+ const x2 = x * ca + z * sa;
415
+ z = -x * sa + z * ca;
416
+ x = x2;
417
+ } else {
418
+ const x2 = x * ca - y * sa;
419
+ y = x * sa + y * ca;
420
+ x = x2;
421
+ }
422
+ }
423
+ project(v, x, y, z, sy, cySpin, st, ct, c, c, R);
424
+ const depth = (v[2] + 1) / 2;
425
+ // The band under the hand inks a touch stronger, so the turn is legible.
426
+ dot(out, v[0], v[1], (0.6 + 1.7 * depth + (inActive ? 0.3 : 0)) * rs, 0.38 + 0.54 * depth + (inActive ? 0.14 : 0));
427
+ }
428
+ }
429
+ }
430
+
431
+ /**
432
+ * `listening` — a waveform rolling through the latitude rings.
433
+ *
434
+ * Two waves at unrelated tempi, so the surface never quite repeats. One wave
435
+ * gives a pulse, which reads as a heartbeat rather than as listening.
436
+ */
437
+ function drawListening(out, size, t, p) {
438
+ 'worklet';
439
+
440
+ const c = size / 2;
441
+ const R = c * 0.874;
442
+ const rs = radiusScale(size) * p.radius;
443
+ const sy = Math.sin(t * 0.18);
444
+ const cySpin = Math.cos(t * 0.18);
445
+ const st = Math.sin(0.38);
446
+ const ct = Math.cos(0.38);
447
+ const v = [0, 0, 0];
448
+ for (let ri = 0; ri <= p.rows; ri++) {
449
+ const lat = -Math.PI / 2 + ri / p.rows * Math.PI;
450
+ const cosLat = Math.cos(lat);
451
+ const sinLat = Math.sin(lat);
452
+ const w = 0.62 * Math.sin(t * 2.1 - ri * 0.52) + 0.38 * Math.sin(t * 1.27 + ri * 0.83);
453
+ const rr = R * (0.88 + 0.105 * w);
454
+ const crest = Math.max(0, w);
455
+ const count = Math.max(1, Math.round(Math.abs(cosLat) * p.density));
456
+ for (let lj = 0; lj < count; lj++) {
457
+ const lon = lj / count * 2 * Math.PI;
458
+ project(v, cosLat * Math.cos(lon) * rr, sinLat * rr, cosLat * Math.sin(lon) * rr, sy, cySpin, st, ct, c, c, 1);
459
+ const depth = (v[2] / R + 1) / 2;
460
+ dot(out, v[0], v[1], (0.6 + 1.7 * depth) * (1 + 0.4 * crest) * rs, 0.34 + 0.56 * depth + 0.1 * crest);
461
+ }
462
+ }
463
+ }
464
+
465
+ /**
466
+ * `composing` — an undulating sash of parallel strands on a great circle,
467
+ * inside a faint dotted shell.
468
+ *
469
+ * The band's own tumble is frozen. Left spinning it competes with the
470
+ * undulation and the two motions cancel into noise; held still, the wave
471
+ * travelling along it is the only thing moving, and it reads as a line of
472
+ * something being written.
473
+ */
474
+ function drawComposing(out, size, t, p) {
475
+ 'worklet';
476
+
477
+ const c = size / 2;
478
+ const R = c * 0.78;
479
+ const rs = radiusScale(size) * p.radius;
480
+ const st = Math.sin(0.3);
481
+ const ct = Math.cos(0.3);
482
+ const v = [0, 0, 0];
483
+
484
+ // The shell: a Fibonacci lattice, which is the only way to scatter points on
485
+ // a sphere evenly without them lining up into visible seams.
486
+ const shell = 70;
487
+ const golden = Math.PI * (3 - Math.sqrt(5));
488
+ for (let i = 0; i < shell; i++) {
489
+ const y = 1 - 2 * (i + 0.5) / shell;
490
+ const rad = Math.sqrt(Math.max(0, 1 - y * y));
491
+ const a = i * golden;
492
+ project(v, rad * Math.cos(a) * R, y * R, rad * Math.sin(a) * R, 0, 1, st, ct, c, c, 1);
493
+ const depth = (v[2] / R + 1) / 2;
494
+ dot(out, v[0], v[1], 0.8 * rs, 0.22 * (0.3 + 0.7 * depth));
495
+ }
496
+ const ta = 0.55;
497
+ const ux = 1;
498
+ const uz = 0;
499
+ const vx = -uz * Math.sin(ta);
500
+ const vy = Math.cos(ta);
501
+ const vz = ux * Math.sin(ta);
502
+ const nx = -uz * vy;
503
+ const ny = uz * vx - ux * vz;
504
+ const nz = ux * vy;
505
+ const lanes = p.rows;
506
+ const half = Math.max(1, (lanes - 1) / 2);
507
+ for (let w = 0; w < lanes; w++) {
508
+ const laneOffset = (w - (lanes - 1) / 2) * 0.075;
509
+ const edge = Math.abs(w - (lanes - 1) / 2) / half;
510
+ for (let k = 0; k < p.density; k++) {
511
+ const a = k / p.density * 2 * Math.PI;
512
+ const wobble = 0.16 * Math.sin(a * 3 - t * 1.7 + w * 0.22) + 0.07 * Math.sin(a * 5 + t * 1.1);
513
+ const off = laneOffset + wobble;
514
+ const ca = Math.cos(a);
515
+ const sa = Math.sin(a);
516
+ const x = ux * ca + vx * sa + nx * off;
517
+ const y = vy * sa + ny * off;
518
+ const z = uz * ca + vz * sa + nz * off;
519
+ const l = Math.max(1e-6, Math.sqrt(x * x + y * y + z * z));
520
+ project(v, x / l * R, y / l * R, z / l * R, 0, 1, st, ct, c, c, 1);
521
+ const depth = (v[2] / R + 1) / 2;
522
+ dot(out, v[0], v[1], (1.1 + 1.7 * depth) * (1 - 0.25 * edge) * rs, (0.48 + 0.44 * depth - 0.18 * edge) * (0.4 + 0.6 * depth));
523
+ }
524
+ }
525
+ }
526
+
527
+ /** The three outlines `shaping` cycles through, as closed paths. */
528
+ const SHAPES = [
529
+ // A circle is sampled, not listed — the marker below says so.
530
+ [], [0, -0.26, 0.24, 0.16, -0.24, 0.16], [0, -0.2, 0.2, -0.2, 0.2, 0.2, -0.2, 0.2, -0.2, -0.2]];
531
+
532
+ /** Point at arc-length fraction `f` around shape `index`. */
533
+ function shapePoint(out, index, f) {
534
+ 'worklet';
535
+
536
+ const verts = SHAPES[index];
537
+ if (!verts.length) {
538
+ // Started at top-centre and walked clockwise, so every shape's dot zero is
539
+ // in the same place and the morph has nothing to unwind.
540
+ const a = -Math.PI / 2 + f * 2 * Math.PI;
541
+ out[0] = Math.cos(a) * 0.24;
542
+ out[1] = Math.sin(a) * 0.24;
543
+ return;
544
+ }
545
+ const n = verts.length / 2;
546
+ let total = 0;
547
+ for (let i = 0; i < n; i++) {
548
+ const j = (i + 1) % n;
549
+ total += Math.hypot(verts[j * 2] - verts[i * 2], verts[j * 2 + 1] - verts[i * 2 + 1]);
550
+ }
551
+ let target = f * total;
552
+ for (let i = 0; i < n; i++) {
553
+ const j = (i + 1) % n;
554
+ const ax = verts[i * 2];
555
+ const ay = verts[i * 2 + 1];
556
+ const bx = verts[j * 2];
557
+ const by = verts[j * 2 + 1];
558
+ const len = Math.hypot(bx - ax, by - ay);
559
+ if (target <= len || i === n - 1) {
560
+ const ff = len ? Math.min(1, target / len) : 0;
561
+ out[0] = ax + (bx - ax) * ff;
562
+ out[1] = ay + (by - ay) * ff;
563
+ return;
564
+ }
565
+ target -= len;
566
+ }
567
+ }
568
+
569
+ /**
570
+ * `shaping` — a dotted outline morphing circle → triangle → square.
571
+ *
572
+ * The two outlines are blended first and the dots laid evenly along the
573
+ * *result*, rather than each dot being tweened from its old place to its new
574
+ * one. Tweening per dot bunches them at the corners halfway through the morph;
575
+ * re-spacing every frame keeps the outline uniform at every instant, which is
576
+ * what makes the shape read as a shape while it is changing.
577
+ */
578
+ function drawShaping(out, size, t, p) {
579
+ 'worklet';
580
+
581
+ const c = size / 2;
582
+ const hold = 1.4;
583
+ const morph = 0.9;
584
+ const seg = hold + morph;
585
+ const shapes = 3;
586
+ const tc = t % (seg * shapes);
587
+ const k = Math.floor(tc / seg);
588
+ const local = tc - k * seg;
589
+ const raw = local > hold ? (local - hold) / morph : 0;
590
+ const m = raw * raw * (3 - 2 * raw);
591
+ const spread = 1.45;
592
+ const samples = 96;
593
+ const px = [];
594
+ const py = [];
595
+ const a = [0, 0];
596
+ const b = [0, 0];
597
+ for (let i = 0; i < samples; i++) {
598
+ const f = i / samples;
599
+ shapePoint(a, k, f);
600
+ shapePoint(b, (k + 1) % shapes, f);
601
+ px.push((a[0] + (b[0] - a[0]) * m) * spread);
602
+ py.push((a[1] + (b[1] - a[1]) * m) * spread);
603
+ }
604
+ const lengths = [];
605
+ let total = 0;
606
+ for (let i = 0; i < samples; i++) {
607
+ const j = (i + 1) % samples;
608
+ const l = Math.hypot(px[j] - px[i], py[j] - py[i]);
609
+ lengths.push(l);
610
+ total += l;
611
+ }
612
+ const n = p.density;
613
+ const r = Math.max(0.35, 0.021 * 1.35 * spread * size * p.radius * 0.4);
614
+ const pulse = 1 + 0.02 * Math.sin(local * 3.1);
615
+ let index = 0;
616
+ let walked = 0;
617
+ for (let i = 0; i < n; i++) {
618
+ const target = i / n * total;
619
+ while (walked + lengths[index] < target && index < samples - 1) {
620
+ walked += lengths[index];
621
+ index++;
622
+ }
623
+ const j = (index + 1) % samples;
624
+ const f = lengths[index] ? Math.min(1, (target - walked) / lengths[index]) : 0;
625
+ const x = (px[index] + (px[j] - px[index]) * f) * pulse;
626
+ const y = (py[index] + (py[j] - py[index]) * f) * pulse;
627
+ dot(out, c + x * size, c + y * size, r, 0.92);
628
+ }
629
+ }
630
+
631
+ /** Builds one frame's worth of bucket paths. */
632
+ function renderFrame(state, size, t, p) {
633
+ 'worklet';
634
+
635
+ const out = [];
636
+ for (let i = 0; i < BUCKETS; i++) out.push('');
637
+ if (state === 'working') drawWorking(out, size, t, p);else if (state === 'searching') drawSearching(out, size, t, p);else if (state === 'solving') drawSolving(out, size, t, p);else if (state === 'listening') drawListening(out, size, t, p);else if (state === 'composing') drawComposing(out, size, t, p);else drawShaping(out, size, t, p);
638
+ return out;
639
+ }
640
+
641
+ /* -------------------------------------------------------------------------- */
642
+ /* Component */
643
+ /* -------------------------------------------------------------------------- */
644
+
645
+ const BUCKET_INDICES = Array.from({
646
+ length: BUCKETS
647
+ }, (_unused, index) => index);
648
+
649
+ /**
650
+ * One ink level's worth of dots, as a single path.
651
+ *
652
+ * It is a component rather than a loop of `useAnimatedProps` in the parent so
653
+ * that each bucket owns exactly one hook — the count is a module constant, but
654
+ * hooks in a loop is still a rule waiting to be broken by the next person who
655
+ * makes it configurable.
656
+ */
657
+ function Bucket({
658
+ index,
659
+ paths,
660
+ ink
661
+ }) {
662
+ const animatedProps = useAnimatedProps(() => ({
663
+ d: paths.value[index] ?? ''
664
+ }));
665
+ return /*#__PURE__*/_jsx(AnimatedPath, {
666
+ animatedProps: animatedProps,
667
+ fill: ink,
668
+ fillOpacity: (index + 0.5) / BUCKETS
669
+ });
670
+ }
671
+ export function ThinkingOrb({
672
+ className,
673
+ state = 'working',
674
+ size = 64,
675
+ speed = 1,
676
+ paused = false,
677
+ color,
678
+ accessibilityLabel,
679
+ style,
680
+ ...props
681
+ }) {
682
+ const reducedMotion = useReducedMotion();
683
+ const foreground = useCSSVariable('--color-foreground');
684
+ const ink = color ?? (typeof foreground === 'string' ? foreground : '#0a0a0a');
685
+ const profile = PROFILES[state][size <= 32 ? 'sm' : 'md'];
686
+ const clock = useSharedValue(0);
687
+ const paths = useSharedValue([]);
688
+ const still = !paused && !reducedMotion;
689
+ const frame = useFrameCallback(info => {
690
+ 'worklet';
691
+
692
+ // Elapsed time is accumulated rather than derived from the total, so
693
+ // `speed` can change mid-animation without the orb jumping to wherever the
694
+ // new rate would have put it by now. A dropped frame is clamped rather than
695
+ // honoured — a 300ms hitch played back at full rate is a lurch.
696
+ const delta = Math.min(info.timeSincePreviousFrame ?? 16, 48) / 1000;
697
+ clock.value += delta * profile.speed * speed;
698
+ paths.value = renderFrame(state, size, clock.value, profile);
699
+ }, false);
700
+ const {
701
+ setActive
702
+ } = frame;
703
+ useEffect(() => {
704
+ setActive(still);
705
+ return () => setActive(false);
706
+ }, [still, setActive]);
707
+
708
+ // A still orb is not an empty one: reduced motion and `paused` both get a
709
+ // representative frame rather than nothing, which is the difference between
710
+ // "not animating" and "broken".
711
+ useEffect(() => {
712
+ if (still) return;
713
+ runOnUI(() => {
714
+ 'worklet';
715
+
716
+ paths.value = renderFrame(state, size, clock.value > 0 ? clock.value : STILL_FRAME, profile);
717
+ })();
718
+ }, [still, state, size, profile, clock, paths]);
719
+ return /*#__PURE__*/_jsx(View, {
720
+ ...props,
721
+ accessibilityRole: "image",
722
+ accessibilityLabel: accessibilityLabel ?? STATE_LABEL[state],
723
+ className: cn('items-center justify-center', className),
724
+ style: [{
725
+ width: size,
726
+ height: size
727
+ }, style],
728
+ children: /*#__PURE__*/_jsx(Svg, {
729
+ width: size,
730
+ height: size,
731
+ viewBox: `0 0 ${size} ${size}`,
732
+ children: BUCKET_INDICES.map(index => /*#__PURE__*/_jsx(Bucket, {
733
+ index: index,
734
+ paths: paths,
735
+ ink: ink
736
+ }, index))
737
+ })
738
+ });
739
+ }
740
+ ThinkingOrb.displayName = 'ThinkingOrb';
741
+ //# sourceMappingURL=index.js.map