upload-motion 0.0.0-stage → 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,3927 @@
1
+ 'use client';
2
+ import { C as formatSpeed, S as formatPercent, _ as getFileLabel, a as UploadError, b as formatBytes, c as toUploadError, d as parseAccept, f as validateFile, g as getFileKind, h as getExtension, i as createUploadStore, l as describeAccept, m as presetCatalog, n as createXhrUploader, o as httpErrorMessage, p as PRESET_INFO, r as computeTotals, s as isUploadError, t as createSimulatedUploader, u as isFileAccepted, v as isPreviewableImage, x as formatDuration, y as clamp01 } from "./core-BUWs_ckM.js";
3
+ import { createContext, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/components/context.tsx
6
+ const UploaderContext = /* @__PURE__ */ createContext(null);
7
+ /** Access the nearest `<Uploader>` / `<FileUpload>` state and actions. */
8
+ function useUploaderContext() {
9
+ const context = useContext(UploaderContext);
10
+ if (!context) throw new Error("upload-motion: this component must be rendered inside <Uploader> or <FileUpload>.");
11
+ return context;
12
+ }
13
+ function getFileHelpers(context, item) {
14
+ return {
15
+ remove: () => context.remove(item.id),
16
+ cancel: () => context.cancel(item.id),
17
+ retry: () => context.retry(item.id),
18
+ pause: () => context.pause(item.id),
19
+ resume: () => context.resume(item.id),
20
+ start: () => context.start(item.id),
21
+ labels: context.labels
22
+ };
23
+ }
24
+ //#endregion
25
+ //#region src/hooks/utils.ts
26
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
27
+ const REDUCED_QUERY = "(prefers-reduced-motion: reduce)";
28
+ function subscribeReducedMotion(callback) {
29
+ if (typeof window === "undefined" || !window.matchMedia) return () => {};
30
+ const query = window.matchMedia(REDUCED_QUERY);
31
+ query.addEventListener("change", callback);
32
+ return () => query.removeEventListener("change", callback);
33
+ }
34
+ /** `true` when the OS asks for reduced motion. Always `false` during SSR. */
35
+ function usePrefersReducedMotion() {
36
+ return useSyncExternalStore(subscribeReducedMotion, () => typeof window !== "undefined" && window.matchMedia ? window.matchMedia(REDUCED_QUERY).matches : false, () => false);
37
+ }
38
+ /**
39
+ * Returns `true` for `duration` ms after `flag` flips from false to true.
40
+ * Does not fire for values that were already true on mount, so items that
41
+ * finished before they rendered don't replay celebrations.
42
+ */
43
+ function useBecameTrue(flag, duration) {
44
+ const previous = useRef(flag);
45
+ const [active, setActive] = useState(false);
46
+ useEffect(() => {
47
+ const was = previous.current;
48
+ previous.current = flag;
49
+ if (!flag || was) {
50
+ if (!flag) setActive(false);
51
+ return;
52
+ }
53
+ setActive(true);
54
+ const timer = setTimeout(() => setActive(false), duration);
55
+ return () => clearTimeout(timer);
56
+ }, [flag, duration]);
57
+ return active;
58
+ }
59
+ /** Deterministic PRNG so decorative randomness is stable between renders and SSR. */
60
+ function seededRandom(seed) {
61
+ let h = 1779033703 ^ seed.length;
62
+ for (let i = 0; i < seed.length; i += 1) {
63
+ h = Math.imul(h ^ seed.charCodeAt(i), 3432918353);
64
+ h = h << 13 | h >>> 19;
65
+ }
66
+ let state = h >>> 0;
67
+ return () => {
68
+ state = state + 1831565813 | 0;
69
+ let t = Math.imul(state ^ state >>> 15, 1 | state);
70
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
71
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
72
+ };
73
+ }
74
+ function cx(...parts) {
75
+ return parts.filter(Boolean).join(" ");
76
+ }
77
+ //#endregion
78
+ //#region src/icons/index.tsx
79
+ function Svg({ size = 20, children, ...props }) {
80
+ return /* @__PURE__ */ jsx("svg", {
81
+ width: size,
82
+ height: size,
83
+ viewBox: "0 0 24 24",
84
+ fill: "none",
85
+ stroke: "currentColor",
86
+ strokeWidth: 1.8,
87
+ strokeLinecap: "round",
88
+ strokeLinejoin: "round",
89
+ "aria-hidden": "true",
90
+ focusable: "false",
91
+ ...props,
92
+ children
93
+ });
94
+ }
95
+ const UploadIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
96
+ ...p,
97
+ children: [
98
+ /* @__PURE__ */ jsx("path", { d: "M12 15V4" }),
99
+ /* @__PURE__ */ jsx("path", { d: "m7 8.5 5-5 5 5" }),
100
+ /* @__PURE__ */ jsx("path", { d: "M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" })
101
+ ]
102
+ });
103
+ const CloudIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
104
+ ...p,
105
+ children: [
106
+ /* @__PURE__ */ jsx("path", { d: "M7 18a4.5 4.5 0 0 1-.6-8.96A6 6 0 0 1 18 8.5a4.5 4.5 0 0 1-.5 9.5" }),
107
+ /* @__PURE__ */ jsx("path", { d: "M12 13v8" }),
108
+ /* @__PURE__ */ jsx("path", { d: "m8.5 16.5 3.5-3.5 3.5 3.5" })
109
+ ]
110
+ });
111
+ const PauseIcon = (p) => /* @__PURE__ */ jsx(Svg, {
112
+ ...p,
113
+ strokeWidth: 2.4,
114
+ children: /* @__PURE__ */ jsx("path", { d: "M9 6v12M15 6v12" })
115
+ });
116
+ const PlayIcon = (p) => /* @__PURE__ */ jsx(Svg, {
117
+ ...p,
118
+ children: /* @__PURE__ */ jsx("path", {
119
+ d: "M8 5.5v13l10.5-6.5z",
120
+ fill: "currentColor",
121
+ stroke: "none"
122
+ })
123
+ });
124
+ const CloseIcon = (p) => /* @__PURE__ */ jsx(Svg, {
125
+ ...p,
126
+ children: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6 6 18" })
127
+ });
128
+ const RetryIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
129
+ ...p,
130
+ children: [/* @__PURE__ */ jsx("path", { d: "M20 11a8 8 0 1 0-2.3 5.7" }), /* @__PURE__ */ jsx("path", { d: "M20 4v7h-7" })]
131
+ });
132
+ const TrashIcon = (p) => /* @__PURE__ */ jsx(Svg, {
133
+ ...p,
134
+ children: /* @__PURE__ */ jsx("path", { d: "M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V4h6v3" })
135
+ });
136
+ const AlertIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
137
+ ...p,
138
+ children: [/* @__PURE__ */ jsx("path", { d: "M12 3 2 20h20L12 3z" }), /* @__PURE__ */ jsx("path", { d: "M12 10v4M12 17.2v.1" })]
139
+ });
140
+ const ExpandIcon = (p) => /* @__PURE__ */ jsx(Svg, {
141
+ ...p,
142
+ children: /* @__PURE__ */ jsx("path", { d: "M14 4h6v6M10 20H4v-6M20 4l-6.5 6.5M4 20l6.5-6.5" })
143
+ });
144
+ const CollapseIcon = (p) => /* @__PURE__ */ jsx(Svg, {
145
+ ...p,
146
+ children: /* @__PURE__ */ jsx("path", { d: "M20 4l-6 6M14 5v5h5M4 20l6-6M10 19v-5H5" })
147
+ });
148
+ const MoreIcon = (p) => /* @__PURE__ */ jsx(Svg, {
149
+ ...p,
150
+ strokeWidth: 2.8,
151
+ children: /* @__PURE__ */ jsx("path", { d: "M12 5.5v.01M12 12v.01M12 18.5v.01" })
152
+ });
153
+ const PlusIcon = (p) => /* @__PURE__ */ jsx(Svg, {
154
+ ...p,
155
+ children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12h14" })
156
+ });
157
+ const ImageIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
158
+ ...p,
159
+ children: [
160
+ /* @__PURE__ */ jsx("rect", {
161
+ x: "3.5",
162
+ y: "3.5",
163
+ width: "17",
164
+ height: "17",
165
+ rx: "3"
166
+ }),
167
+ /* @__PURE__ */ jsx("circle", {
168
+ cx: "9",
169
+ cy: "9",
170
+ r: "1.8"
171
+ }),
172
+ /* @__PURE__ */ jsx("path", { d: "m20.5 15.5-5-5-9 9" })
173
+ ]
174
+ });
175
+ const VideoIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
176
+ ...p,
177
+ children: [/* @__PURE__ */ jsx("circle", {
178
+ cx: "12",
179
+ cy: "12",
180
+ r: "8.5"
181
+ }), /* @__PURE__ */ jsx("path", { d: "M10.2 8.8v6.4l5.2-3.2z" })]
182
+ });
183
+ const MusicIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
184
+ ...p,
185
+ children: [
186
+ /* @__PURE__ */ jsx("path", { d: "M9 17.5V6l11-2v11.5" }),
187
+ /* @__PURE__ */ jsx("circle", {
188
+ cx: "6.5",
189
+ cy: "17.5",
190
+ r: "2.5"
191
+ }),
192
+ /* @__PURE__ */ jsx("circle", {
193
+ cx: "17.5",
194
+ cy: "15.5",
195
+ r: "2.5"
196
+ })
197
+ ]
198
+ });
199
+ const DocumentIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
200
+ ...p,
201
+ children: [/* @__PURE__ */ jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ jsx("path", { d: "M14 3v5h5M9 13h6M9 17h4" })]
202
+ });
203
+ const SheetIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
204
+ ...p,
205
+ children: [/* @__PURE__ */ jsx("rect", {
206
+ x: "4",
207
+ y: "4",
208
+ width: "16",
209
+ height: "16",
210
+ rx: "2"
211
+ }), /* @__PURE__ */ jsx("path", { d: "M4 10h16M4 15h16M10 4v16" })]
212
+ });
213
+ const SlidesIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
214
+ ...p,
215
+ children: [/* @__PURE__ */ jsx("rect", {
216
+ x: "3",
217
+ y: "4",
218
+ width: "18",
219
+ height: "12",
220
+ rx: "2"
221
+ }), /* @__PURE__ */ jsx("path", { d: "M12 16v4M8 20h8" })]
222
+ });
223
+ const ArchiveIcon = (p) => /* @__PURE__ */ jsxs(Svg, {
224
+ ...p,
225
+ children: [
226
+ /* @__PURE__ */ jsx("rect", {
227
+ x: "4",
228
+ y: "3",
229
+ width: "16",
230
+ height: "18",
231
+ rx: "2"
232
+ }),
233
+ /* @__PURE__ */ jsx("path", { d: "M12 3v2M12 7v2M12 11v2" }),
234
+ /* @__PURE__ */ jsx("rect", {
235
+ x: "10",
236
+ y: "14",
237
+ width: "4",
238
+ height: "4",
239
+ rx: "1"
240
+ })
241
+ ]
242
+ });
243
+ const CodeIcon = (p) => /* @__PURE__ */ jsx(Svg, {
244
+ ...p,
245
+ children: /* @__PURE__ */ jsx("path", { d: "m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14" })
246
+ });
247
+ const DataIcon = (p) => /* @__PURE__ */ jsx(Svg, {
248
+ ...p,
249
+ children: /* @__PURE__ */ jsx("path", { d: "M8 4c-2 0-2 1.5-2 3s0 3-2 5c2 2 2 3.5 2 5s0 3 2 3M16 4c2 0 2 1.5 2 3s0 3 2 5c-2 2-2 3.5-2 5s0 3-2 3" })
250
+ });
251
+ const TextIcon = (p) => /* @__PURE__ */ jsx(Svg, {
252
+ ...p,
253
+ children: /* @__PURE__ */ jsx("path", { d: "M5 6h14M5 10h14M5 14h10M5 18h7" })
254
+ });
255
+ const FileIconGlyph = (p) => /* @__PURE__ */ jsxs(Svg, {
256
+ ...p,
257
+ children: [/* @__PURE__ */ jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ jsx("path", { d: "M14 3v5h5" })]
258
+ });
259
+ const KIND_ICONS = {
260
+ image: ImageIcon,
261
+ video: VideoIcon,
262
+ audio: MusicIcon,
263
+ pdf: DocumentIcon,
264
+ document: DocumentIcon,
265
+ spreadsheet: SheetIcon,
266
+ presentation: SlidesIcon,
267
+ archive: ArchiveIcon,
268
+ code: CodeIcon,
269
+ data: DataIcon,
270
+ text: TextIcon,
271
+ unknown: FileIconGlyph
272
+ };
273
+ //#endregion
274
+ //#region src/components/Dropzone.tsx
275
+ /** Drop target. Click anywhere (outside inner controls) to open the file dialog; paste works while focused. */
276
+ function UploadDropzone({ children, className, style, focusable, trackPointer, ...rest }) {
277
+ const { dropzone, labels, hintId, disabled } = useUploaderContext();
278
+ return /* @__PURE__ */ jsx("div", {
279
+ ...dropzone.getRootProps({
280
+ focusable,
281
+ trackPointer,
282
+ "aria-label": focusable ? rest["aria-label"] ?? labels.zoneLabel : void 0,
283
+ "aria-describedby": hintId
284
+ }),
285
+ className: cx("um-zone", className),
286
+ style,
287
+ "data-um-disabled": disabled || void 0,
288
+ children: children ?? /* @__PURE__ */ jsx(DropzoneContent, {})
289
+ });
290
+ }
291
+ /** Default zone content: icon, headline, browse button and limits hint. */
292
+ function DropzoneContent({ icon, title }) {
293
+ const { dropzone, labels } = useUploaderContext();
294
+ const headline = dropzone.dragState === "reject" ? labels.dropReject : dropzone.dragState === "accept" ? labels.dropActive : title ?? labels.dropTitle;
295
+ return /* @__PURE__ */ jsxs("div", {
296
+ className: "um-zone__content",
297
+ children: [
298
+ /* @__PURE__ */ jsx("span", {
299
+ className: "um-zone__icon",
300
+ children: icon ?? /* @__PURE__ */ jsx(UploadIcon, { size: 22 })
301
+ }),
302
+ /* @__PURE__ */ jsx("span", {
303
+ className: "um-zone__title",
304
+ children: headline
305
+ }),
306
+ /* @__PURE__ */ jsx(BrowseButton, { variant: "link" }),
307
+ /* @__PURE__ */ jsx(UploadHint, {})
308
+ ]
309
+ });
310
+ }
311
+ function UploadHint({ className }) {
312
+ const { hint, hintId } = useUploaderContext();
313
+ return /* @__PURE__ */ jsx("span", {
314
+ id: hintId,
315
+ className: cx("um-hint", className),
316
+ children: hint
317
+ });
318
+ }
319
+ /** Keyboard-accessible trigger for the native file dialog. */
320
+ function BrowseButton({ children, className, variant = "soft", ...rest }) {
321
+ const { openFileDialog, labels, disabled, hintId } = useUploaderContext();
322
+ return /* @__PURE__ */ jsx("button", {
323
+ type: "button",
324
+ className: cx("um-btn", `um-btn--${variant}`, className),
325
+ onClick: (event) => {
326
+ event.stopPropagation();
327
+ openFileDialog();
328
+ },
329
+ disabled,
330
+ "aria-describedby": hintId,
331
+ "aria-label": rest["aria-label"],
332
+ children: children ?? labels.browse
333
+ });
334
+ }
335
+ /** Starts pending uploads in manual mode (`autoUpload={false}`). Shows aggregate progress while uploading. */
336
+ function StartUploadButton({ className, children }) {
337
+ const { totals, start, labels, transport } = useUploaderContext();
338
+ const uploading = totals.active > 0;
339
+ const percent = Math.round(totals.progress * 100);
340
+ const disabled = transport === "none" || totals.pending === 0 && !uploading;
341
+ const done = !uploading && totals.pending === 0 && totals.isComplete;
342
+ return /* @__PURE__ */ jsxs("button", {
343
+ type: "button",
344
+ className: cx("um-btn", "um-btn--solid", "um-start-btn", className),
345
+ "data-state": uploading ? "uploading" : done ? "done" : "idle",
346
+ style: { "--um-p": totals.progress },
347
+ disabled,
348
+ "aria-disabled": uploading || void 0,
349
+ onClick: (event) => {
350
+ event.stopPropagation();
351
+ if (!uploading) start();
352
+ },
353
+ children: [/* @__PURE__ */ jsx("span", {
354
+ className: "um-start-btn__fill",
355
+ "aria-hidden": "true"
356
+ }), /* @__PURE__ */ jsx("span", {
357
+ className: "um-start-btn__label",
358
+ children: uploading ? `${labels.status.uploading} ${percent}%` : done ? `✓ ${labels.status.success}` : children ?? (totals.pending > 1 ? `${labels.upload} ${totals.pending} files` : labels.upload)
359
+ })]
360
+ });
361
+ }
362
+ //#endregion
363
+ //#region src/hooks/useListAnimation.ts
364
+ /**
365
+ * Keeps removed items rendered for `exitMs` so they can animate out.
366
+ * Removed items are re-inserted at their previous index.
367
+ */
368
+ function usePresence(items, exitMs) {
369
+ const previous = useRef(items);
370
+ const [exiting, setExiting] = useState(() => /* @__PURE__ */ new Map());
371
+ const timers = useRef(/* @__PURE__ */ new Set());
372
+ useIsomorphicLayoutEffect(() => {
373
+ const before = previous.current;
374
+ previous.current = items;
375
+ if (exitMs <= 0 || before === items) return;
376
+ const ids = new Set(items.map((item) => item.id));
377
+ const removed = before.flatMap((item, index) => ids.has(item.id) ? [] : [{
378
+ item,
379
+ index
380
+ }]);
381
+ if (removed.length === 0) return;
382
+ setExiting((map) => {
383
+ const next = new Map(map);
384
+ for (const entry of removed) next.set(entry.item.id, entry);
385
+ return next;
386
+ });
387
+ const timer = setTimeout(() => {
388
+ timers.current.delete(timer);
389
+ setExiting((map) => {
390
+ const next = new Map(map);
391
+ for (const entry of removed) next.delete(entry.item.id);
392
+ return next;
393
+ });
394
+ }, exitMs);
395
+ timers.current.add(timer);
396
+ }, [items, exitMs]);
397
+ useEffect(() => {
398
+ const pending = timers.current;
399
+ return () => {
400
+ for (const timer of pending) clearTimeout(timer);
401
+ };
402
+ }, []);
403
+ const result = items.map((item) => ({
404
+ item,
405
+ exiting: false
406
+ }));
407
+ if (exiting.size > 0) {
408
+ const sorted = [...exiting.values()].sort((a, b) => a.index - b.index);
409
+ for (const entry of sorted) {
410
+ if (result.some((existing) => existing.item.id === entry.item.id)) continue;
411
+ result.splice(Math.min(entry.index, result.length), 0, {
412
+ item: entry.item,
413
+ exiting: true
414
+ });
415
+ }
416
+ }
417
+ return result;
418
+ }
419
+ /**
420
+ * FLIP layout animation: when the set/order of `[data-flip-id]` children
421
+ * changes, children animate from their old offset to the new one using WAAPI.
422
+ */
423
+ function useFlip(containerRef, layoutKey, enabled, duration = 420) {
424
+ const positions = useRef(/* @__PURE__ */ new Map());
425
+ useIsomorphicLayoutEffect(() => {
426
+ const container = containerRef.current;
427
+ if (!container) return;
428
+ const nodes = container.querySelectorAll("[data-flip-id]");
429
+ const next = /* @__PURE__ */ new Map();
430
+ nodes.forEach((node) => {
431
+ const id = node.dataset.flipId;
432
+ const position = {
433
+ x: node.offsetLeft,
434
+ y: node.offsetTop
435
+ };
436
+ next.set(id, position);
437
+ const before = positions.current.get(id);
438
+ if (!enabled || !before || typeof node.animate !== "function") return;
439
+ const dx = before.x - position.x;
440
+ const dy = before.y - position.y;
441
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return;
442
+ node.animate([{ transform: `translate(${dx}px, ${dy}px)` }, { transform: "translate(0, 0)" }], {
443
+ duration,
444
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
445
+ composite: "add"
446
+ });
447
+ });
448
+ positions.current = next;
449
+ }, [
450
+ layoutKey,
451
+ enabled,
452
+ duration
453
+ ]);
454
+ }
455
+ //#endregion
456
+ //#region src/components/Effects.tsx
457
+ /** Stroke-drawn checkmark inside a circle. */
458
+ function AnimatedCheck({ size = 24, className }) {
459
+ return /* @__PURE__ */ jsxs("svg", {
460
+ className: cx("um-check", className),
461
+ width: size,
462
+ height: size,
463
+ viewBox: "0 0 24 24",
464
+ "aria-hidden": "true",
465
+ children: [/* @__PURE__ */ jsx("circle", {
466
+ className: "um-check__circle",
467
+ cx: "12",
468
+ cy: "12",
469
+ r: "10.5",
470
+ pathLength: 1
471
+ }), /* @__PURE__ */ jsx("path", {
472
+ className: "um-check__mark",
473
+ d: "m7.2 12.4 3.2 3.2 6.4-7",
474
+ pathLength: 1
475
+ })]
476
+ });
477
+ }
478
+ /**
479
+ * One-shot particle burst built from a handful of spans animated with CSS
480
+ * transforms. Mount it (e.g. with a key) to play; renders nothing for reduced
481
+ * motion or `particles: 0`.
482
+ */
483
+ function Burst({ count = 14, seed = "burst", spread = 56, className, shape = "dot" }) {
484
+ const { motion } = useUploaderContext();
485
+ const total = motion.reduced ? 0 : Math.round(count * motion.particles);
486
+ const particles = useMemo(() => {
487
+ const random = seededRandom(seed);
488
+ return Array.from({ length: total }, (_, index) => {
489
+ const angle = index / Math.max(total, 1) * Math.PI * 2 + random() * .6;
490
+ const distance = spread * (.55 + random() * .6);
491
+ return {
492
+ x: Math.cos(angle) * distance,
493
+ y: Math.sin(angle) * distance,
494
+ rotate: angle * 180 / Math.PI + 90,
495
+ delay: random() * 60,
496
+ scale: .6 + random() * .8,
497
+ hue: random() > .5 ? "var(--um-accent)" : "var(--um-accent-2)"
498
+ };
499
+ });
500
+ }, [
501
+ total,
502
+ seed,
503
+ spread
504
+ ]);
505
+ if (total === 0) return null;
506
+ return /* @__PURE__ */ jsx("span", {
507
+ className: cx("um-burst", className),
508
+ "data-shape": shape,
509
+ "aria-hidden": "true",
510
+ children: particles.map((particle, index) => /* @__PURE__ */ jsx("span", {
511
+ className: "um-burst__p",
512
+ style: {
513
+ "--x": `${particle.x.toFixed(1)}px`,
514
+ "--y": `${particle.y.toFixed(1)}px`,
515
+ "--r": `${particle.rotate.toFixed(0)}deg`,
516
+ "--s": particle.scale.toFixed(2),
517
+ "--c": particle.hue,
518
+ animationDelay: `${particle.delay.toFixed(0)}ms`
519
+ }
520
+ }, index))
521
+ });
522
+ }
523
+ //#endregion
524
+ //#region src/hooks/useMedia.ts
525
+ const DEFAULT_PREVIEW_LIMIT = 25e6;
526
+ /**
527
+ * Object URL for an image preview, revoked automatically on unmount or when the
528
+ * file changes. Returns `null` for non-images and files above `maxSize` (large
529
+ * images are expensive to decode, so they get an icon instead).
530
+ */
531
+ function useFilePreview(item, maxSize = DEFAULT_PREVIEW_LIMIT) {
532
+ const file = item?.file ?? null;
533
+ const [url, setUrl] = useState(null);
534
+ useEffect(() => {
535
+ if (!file || file.size > maxSize || !isPreviewableImage(file) || typeof URL.createObjectURL !== "function") {
536
+ setUrl(null);
537
+ return;
538
+ }
539
+ const objectUrl = URL.createObjectURL(file);
540
+ setUrl(objectUrl);
541
+ return () => URL.revokeObjectURL(objectUrl);
542
+ }, [file, maxSize]);
543
+ return url;
544
+ }
545
+ /**
546
+ * Extracts `count` evenly spaced frames from a local video as small JPEG data
547
+ * URLs. Everything happens in the browser; unsupported codecs yield `[]`.
548
+ */
549
+ function useVideoFrames(file, count, enabled = true) {
550
+ const [frames, setFrames] = useState([]);
551
+ useEffect(() => {
552
+ setFrames([]);
553
+ if (!enabled || !file || typeof document === "undefined" || typeof URL.createObjectURL !== "function") return;
554
+ if (!file.type.startsWith("video/") && !/\.(mp4|mov|webm|m4v|ogv)$/i.test(file.name)) return;
555
+ let cancelled = false;
556
+ const url = URL.createObjectURL(file);
557
+ const video = document.createElement("video");
558
+ video.muted = true;
559
+ video.playsInline = true;
560
+ video.preload = "auto";
561
+ video.src = url;
562
+ const once = (event, timeout = 6e3) => new Promise((resolve, reject) => {
563
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("timeout")), timeout);
564
+ const done = () => {
565
+ clearTimeout(timer);
566
+ video.removeEventListener("error", fail);
567
+ resolve();
568
+ };
569
+ const fail = () => {
570
+ clearTimeout(timer);
571
+ video.removeEventListener(event, done);
572
+ reject(/* @__PURE__ */ new Error("video error"));
573
+ };
574
+ video.addEventListener(event, done, { once: true });
575
+ video.addEventListener("error", fail, { once: true });
576
+ });
577
+ const run = async () => {
578
+ await once("loadeddata");
579
+ let duration = video.duration;
580
+ if (!Number.isFinite(duration)) {
581
+ video.currentTime = 1e7;
582
+ await once("seeked").catch(() => void 0);
583
+ duration = Number.isFinite(video.duration) ? video.duration : 0;
584
+ }
585
+ const width = 160;
586
+ const height = video.videoWidth ? Math.round(width * video.videoHeight / video.videoWidth) : 90;
587
+ const canvas = document.createElement("canvas");
588
+ canvas.width = width;
589
+ canvas.height = height;
590
+ const context = canvas.getContext("2d");
591
+ if (!context) return;
592
+ const result = [];
593
+ for (let index = 0; index < count; index += 1) {
594
+ const time = duration > 0 ? Math.min(duration - .05, duration * (index + .5) / count) : 0;
595
+ video.currentTime = Math.max(0, time);
596
+ await once("seeked");
597
+ if (cancelled) return;
598
+ context.drawImage(video, 0, 0, width, height);
599
+ result.push(canvas.toDataURL("image/jpeg", .72));
600
+ setFrames([...result]);
601
+ }
602
+ };
603
+ let released = false;
604
+ const release = () => {
605
+ if (released) return;
606
+ released = true;
607
+ video.removeAttribute("src");
608
+ video.load();
609
+ URL.revokeObjectURL(url);
610
+ };
611
+ run().catch(() => void 0).finally(release);
612
+ return () => {
613
+ cancelled = true;
614
+ release();
615
+ };
616
+ }, [
617
+ file,
618
+ count,
619
+ enabled
620
+ ]);
621
+ return frames;
622
+ }
623
+ /** Deterministic pseudo waveform (used until/unless real audio decoding succeeds). */
624
+ function pseudoPeaks(seed, bars) {
625
+ const random = seededRandom(seed);
626
+ const peaks = [];
627
+ let value = .5;
628
+ for (let i = 0; i < bars; i += 1) {
629
+ value = value * .55 + random() * .45;
630
+ const envelope = .55 + .45 * Math.sin(i / bars * Math.PI);
631
+ peaks.push(Math.max(.12, Math.min(1, value * envelope * 1.35)));
632
+ }
633
+ return peaks;
634
+ }
635
+ const AUDIO_DECODE_LIMIT = 3e7;
636
+ /**
637
+ * Real waveform peaks decoded with the Web Audio API for audio files up to
638
+ * 30 MB; a stable pseudo waveform otherwise. `real` tells you which you got.
639
+ */
640
+ function useAudioPeaks(file, bars, enabled = true) {
641
+ const fallback = useMemo(() => pseudoPeaks(file ? `${file.name}:${file.size}` : "empty", bars), [file, bars]);
642
+ const [decoded, setDecoded] = useState(null);
643
+ useEffect(() => {
644
+ if (!enabled || !file || file.size > AUDIO_DECODE_LIMIT) return;
645
+ if (!file.type.startsWith("audio/") && !/\.(mp3|wav|ogg|m4a|aac|flac|opus)$/i.test(file.name)) return;
646
+ const AudioContextClass = typeof window !== "undefined" ? window.AudioContext ?? window.webkitAudioContext : void 0;
647
+ if (!AudioContextClass) return;
648
+ let cancelled = false;
649
+ const context = new AudioContextClass();
650
+ file.arrayBuffer().then((buffer) => context.decodeAudioData(buffer)).then((audio) => {
651
+ if (cancelled) return;
652
+ const data = audio.getChannelData(0);
653
+ const bucket = Math.max(1, Math.floor(data.length / bars));
654
+ const peaks = [];
655
+ let max = 0;
656
+ for (let bar = 0; bar < bars; bar += 1) {
657
+ let peak = 0;
658
+ const start = bar * bucket;
659
+ for (let i = start; i < Math.min(start + bucket, data.length); i += 16) {
660
+ const value = Math.abs(data[i]);
661
+ if (value > peak) peak = value;
662
+ }
663
+ peaks.push(peak);
664
+ if (peak > max) max = peak;
665
+ }
666
+ setDecoded({
667
+ file,
668
+ peaks: peaks.map((peak) => Math.max(.08, max > 0 ? peak / max : 0))
669
+ });
670
+ }).catch(() => void 0).finally(() => {
671
+ context.close().catch(() => void 0);
672
+ });
673
+ return () => {
674
+ cancelled = true;
675
+ };
676
+ }, [
677
+ file,
678
+ bars,
679
+ enabled
680
+ ]);
681
+ return decoded && decoded.file === file && decoded.peaks.length === bars ? {
682
+ peaks: decoded.peaks,
683
+ real: true
684
+ } : {
685
+ peaks: fallback,
686
+ real: false
687
+ };
688
+ }
689
+ const TEXT_KINDS = /* @__PURE__ */ new Set([
690
+ "text",
691
+ "code",
692
+ "data",
693
+ "spreadsheet"
694
+ ]);
695
+ /** First lines of small text-like files, for document previews. `null` when not applicable. */
696
+ function useTextSnippet(item, lines = 8) {
697
+ const file = item?.file ?? null;
698
+ const readable = Boolean(item && TEXT_KINDS.has(item.kind) && item.size < 2e6 && !/\.xlsx?$/i.test(item.file.name));
699
+ const [snippet, setSnippet] = useState(null);
700
+ useEffect(() => {
701
+ setSnippet(null);
702
+ if (!file || !readable || typeof file.slice !== "function") return;
703
+ let cancelled = false;
704
+ file.slice(0, 2048).text().then((text) => {
705
+ if (cancelled) return;
706
+ setSnippet(text.split(/\r?\n/).map((line) => line.replace(/\t/g, " ").slice(0, 64)).filter((line) => line.trim().length > 0).slice(0, lines));
707
+ }).catch(() => void 0);
708
+ return () => {
709
+ cancelled = true;
710
+ };
711
+ }, [
712
+ file,
713
+ readable,
714
+ lines
715
+ ]);
716
+ return snippet;
717
+ }
718
+ //#endregion
719
+ //#region src/components/FileVisual.tsx
720
+ /** Paper-shaped file icon with a kind glyph and coloured extension badge. */
721
+ function FileIcon({ kind, label, size = 40, className }) {
722
+ const Glyph = KIND_ICONS[kind];
723
+ return /* @__PURE__ */ jsxs("span", {
724
+ className: cx("um-ficon", className),
725
+ "data-kind": kind,
726
+ style: { "--um-ficon-size": `${size}px` },
727
+ "aria-hidden": "true",
728
+ children: [/* @__PURE__ */ jsx("span", {
729
+ className: "um-ficon__paper",
730
+ children: /* @__PURE__ */ jsx(Glyph, { size: Math.round(size * .42) })
731
+ }), label ? /* @__PURE__ */ jsx("span", {
732
+ className: "um-ficon__badge",
733
+ children: label
734
+ }) : null]
735
+ });
736
+ }
737
+ /** Image thumbnail for previewable images, otherwise a `FileIcon`. Object URLs are revoked automatically. */
738
+ function FilePreview({ item, size = 44, maxPreviewSize, className }) {
739
+ const url = useFilePreview(item, maxPreviewSize);
740
+ const [loaded, setLoaded] = useState(false);
741
+ if (!url) return /* @__PURE__ */ jsx(FileIcon, {
742
+ kind: item.kind,
743
+ label: getFileLabel(item),
744
+ size,
745
+ className
746
+ });
747
+ return /* @__PURE__ */ jsx("span", {
748
+ className: cx("um-thumb", className),
749
+ style: { "--um-thumb-size": `${size}px` },
750
+ "data-loaded": loaded || void 0,
751
+ children: /* @__PURE__ */ jsx("img", {
752
+ src: url,
753
+ alt: "",
754
+ decoding: "async",
755
+ loading: "lazy",
756
+ draggable: false,
757
+ onLoad: () => setLoaded(true)
758
+ })
759
+ });
760
+ }
761
+ //#endregion
762
+ //#region src/components/Progress.tsx
763
+ /** Builds a sine-like quadratic wave: `width` px wide, one crest every `period` px. */
764
+ function wavePath(width, period, amplitude, baseline) {
765
+ const half = period / 2;
766
+ let d = `M0 ${baseline} Q ${half / 2} ${baseline - amplitude} ${half} ${baseline}`;
767
+ for (let x = half; x < width; x += half) d += ` T ${x + half} ${baseline}`;
768
+ return d;
769
+ }
770
+ const WAVE_PATH = /* @__PURE__ */ wavePath(240, 30, 5, 6);
771
+ /**
772
+ * Progress indicator with four styles. Animates with transforms/clip-path only,
773
+ * so it stays cheap even with many concurrent uploads.
774
+ */
775
+ function UploadProgress({ value, status = "uploading", variant = "bar", label, valueText, size = 36, className }) {
776
+ const progress = clamp01(value);
777
+ const percent = Math.round(progress * 100);
778
+ const a11y = {
779
+ role: "progressbar",
780
+ "aria-valuemin": 0,
781
+ "aria-valuemax": 100,
782
+ "aria-valuenow": percent,
783
+ "aria-valuetext": valueText ?? `${percent}%`,
784
+ "aria-label": label
785
+ };
786
+ const style = { "--um-p": progress };
787
+ if (variant === "ring") {
788
+ const stroke = 3;
789
+ const radius = (size - stroke) / 2;
790
+ const circumference = 2 * Math.PI * radius;
791
+ return /* @__PURE__ */ jsxs("span", {
792
+ ...a11y,
793
+ className: cx("um-progress", "um-progress--ring", className),
794
+ "data-status": status,
795
+ style,
796
+ children: [/* @__PURE__ */ jsxs("svg", {
797
+ width: size,
798
+ height: size,
799
+ viewBox: `0 0 ${size} ${size}`,
800
+ "aria-hidden": "true",
801
+ children: [/* @__PURE__ */ jsx("circle", {
802
+ className: "um-progress__track",
803
+ cx: size / 2,
804
+ cy: size / 2,
805
+ r: radius,
806
+ strokeWidth: stroke
807
+ }), /* @__PURE__ */ jsx("circle", {
808
+ className: "um-progress__arc",
809
+ cx: size / 2,
810
+ cy: size / 2,
811
+ r: radius,
812
+ strokeWidth: stroke,
813
+ strokeDasharray: circumference,
814
+ strokeDashoffset: circumference * (1 - progress)
815
+ })]
816
+ }), /* @__PURE__ */ jsx("span", {
817
+ className: "um-progress__label",
818
+ "aria-hidden": "true",
819
+ children: percent
820
+ })]
821
+ });
822
+ }
823
+ if (variant === "wave") return /* @__PURE__ */ jsx("span", {
824
+ ...a11y,
825
+ className: cx("um-progress", "um-progress--wave", className),
826
+ "data-status": status,
827
+ style,
828
+ children: /* @__PURE__ */ jsx("span", {
829
+ className: "um-progress__wave-clip",
830
+ "aria-hidden": "true",
831
+ children: /* @__PURE__ */ jsx("svg", {
832
+ className: "um-progress__wave",
833
+ viewBox: "0 0 240 12",
834
+ preserveAspectRatio: "none",
835
+ children: /* @__PURE__ */ jsx("path", { d: WAVE_PATH })
836
+ })
837
+ })
838
+ });
839
+ return /* @__PURE__ */ jsx("span", {
840
+ ...a11y,
841
+ className: cx("um-progress", variant === "beam" ? "um-progress--beam" : "um-progress--bar", className),
842
+ "data-status": status,
843
+ style,
844
+ children: /* @__PURE__ */ jsx("span", {
845
+ className: "um-progress__fill",
846
+ "aria-hidden": "true"
847
+ })
848
+ });
849
+ }
850
+ //#endregion
851
+ //#region src/components/FileItem.tsx
852
+ /** Plain-language status line, e.g. "45% · 2.1 MB/s · 3 seconds left". */
853
+ function describeItemStatus(item, labels, transport) {
854
+ switch (item.status) {
855
+ case "uploading": {
856
+ if (item.progress >= 1) return labels.finishing;
857
+ const parts = [formatPercent(item.progress)];
858
+ if (item.speed > 0) parts.push(formatSpeed(item.speed));
859
+ if (item.eta !== null && item.eta > .5) parts.push(labels.timeLeft(formatDuration(item.eta)));
860
+ return parts.join(" · ");
861
+ }
862
+ case "queued":
863
+ if (item.retryAt !== null) {
864
+ const seconds = Math.max(1, Math.ceil((item.retryAt - Date.now()) / 1e3));
865
+ return `${labels.retryingIn(seconds)}${item.error ? ` ${item.error.message}` : ""}`;
866
+ }
867
+ return labels.status.queued;
868
+ case "error": return item.error?.message ?? labels.status.error;
869
+ case "pending": return transport === "none" ? labels.noHandler : labels.status.pending;
870
+ default: return labels.status[item.status];
871
+ }
872
+ }
873
+ /** Accessible value text for progress bars. */
874
+ function progressValueText(item, labels) {
875
+ const percent = formatPercent(item.progress);
876
+ if (item.status !== "uploading" || item.eta === null) return `${percent}, ${labels.status[item.status]}`;
877
+ return `${percent}, ${labels.timeLeft(formatDuration(item.eta))}`;
878
+ }
879
+ function ItemStatus({ item, className }) {
880
+ const { labels, transport } = useUploaderContext();
881
+ const text = describeItemStatus(item, labels, transport);
882
+ return /* @__PURE__ */ jsxs("span", {
883
+ className: cx("um-status", className),
884
+ "data-status": item.status,
885
+ children: [item.status === "error" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(AlertIcon, { size: 13 }), /* @__PURE__ */ jsxs("span", {
886
+ className: "um-sr-only",
887
+ children: [labels.status.error, ": "]
888
+ })] }) : null, /* @__PURE__ */ jsx("span", {
889
+ className: "um-status__text",
890
+ children: text
891
+ })]
892
+ });
893
+ }
894
+ function IconButton({ label, onClick, children, tone = "default" }) {
895
+ return /* @__PURE__ */ jsx("button", {
896
+ type: "button",
897
+ className: "um-icon-btn",
898
+ "data-tone": tone,
899
+ "aria-label": label,
900
+ title: label,
901
+ onClick: (event) => {
902
+ event.stopPropagation();
903
+ onClick();
904
+ },
905
+ children
906
+ });
907
+ }
908
+ /** Contextual actions for one file: start, pause/resume, cancel, retry, remove. */
909
+ function ItemActions({ item, className, allowPause = false }) {
910
+ const context = useUploaderContext();
911
+ const { labels } = context;
912
+ const name = item.name;
913
+ const buttons = [];
914
+ if (item.status === "pending" && context.transport !== "none") buttons.push(/* @__PURE__ */ jsx(IconButton, {
915
+ label: `${labels.upload} ${name}`,
916
+ onClick: () => context.start(item.id),
917
+ tone: "accent",
918
+ children: /* @__PURE__ */ jsx(UploadIcon, { size: 16 })
919
+ }, "start"));
920
+ if (item.status === "uploading" && allowPause) buttons.push(/* @__PURE__ */ jsx(IconButton, {
921
+ label: `${labels.pause} ${name}`,
922
+ onClick: () => context.pause(item.id),
923
+ children: /* @__PURE__ */ jsx(PauseIcon, { size: 15 })
924
+ }, "pause"));
925
+ if (item.status === "paused") buttons.push(/* @__PURE__ */ jsx(IconButton, {
926
+ label: `${labels.resume} ${name}`,
927
+ onClick: () => context.resume(item.id),
928
+ tone: "accent",
929
+ children: /* @__PURE__ */ jsx(PlayIcon, { size: 15 })
930
+ }, "resume"));
931
+ if (item.status === "error" || item.status === "canceled") buttons.push(/* @__PURE__ */ jsx(IconButton, {
932
+ label: `${labels.retry} ${name}`,
933
+ onClick: () => context.retry(item.id),
934
+ tone: "accent",
935
+ children: /* @__PURE__ */ jsx(RetryIcon, {
936
+ size: 16,
937
+ className: "um-retry-icon"
938
+ })
939
+ }, "retry"));
940
+ if (item.status === "uploading" || item.status === "queued" || item.status === "paused") buttons.push(/* @__PURE__ */ jsx(IconButton, {
941
+ label: `${labels.cancel} ${name}`,
942
+ onClick: () => context.cancel(item.id),
943
+ children: /* @__PURE__ */ jsx(CloseIcon, { size: 16 })
944
+ }, "cancel"));
945
+ else buttons.push(/* @__PURE__ */ jsx(IconButton, {
946
+ label: `${labels.remove} ${name}`,
947
+ onClick: () => context.remove(item.id),
948
+ tone: "danger",
949
+ children: item.status === "success" ? /* @__PURE__ */ jsx(CloseIcon, { size: 16 }) : /* @__PURE__ */ jsx(TrashIcon, { size: 16 })
950
+ }, "remove"));
951
+ return /* @__PURE__ */ jsx("div", {
952
+ className: cx("um-actions", className),
953
+ children: buttons
954
+ });
955
+ }
956
+ /** The default animated file row used by list-based presets. */
957
+ const FileCard = /* @__PURE__ */ memo(function FileCard({ item, progressVariant = "bar", className, media, allowPause, celebrate = true }) {
958
+ const { labels } = useUploaderContext();
959
+ const justSucceeded = useBecameTrue(item.status === "success", 1200);
960
+ const justFailed = useBecameTrue(item.status === "error", 700);
961
+ const showProgress = item.status !== "pending" && item.status !== "canceled";
962
+ return /* @__PURE__ */ jsxs("div", {
963
+ className: cx("um-card", className),
964
+ "data-status": item.status,
965
+ "data-kind": item.kind,
966
+ "data-celebrate": justSucceeded || void 0,
967
+ "data-shake": justFailed || void 0,
968
+ children: [
969
+ /* @__PURE__ */ jsxs("div", {
970
+ className: "um-card__media",
971
+ children: [media ?? /* @__PURE__ */ jsx(FilePreview, {
972
+ item,
973
+ size: 42
974
+ }), item.status === "success" ? /* @__PURE__ */ jsx(AnimatedCheck, {
975
+ size: 18,
976
+ className: "um-card__check"
977
+ }) : null]
978
+ }),
979
+ /* @__PURE__ */ jsxs("div", {
980
+ className: "um-card__body",
981
+ children: [
982
+ /* @__PURE__ */ jsxs("div", {
983
+ className: "um-card__row",
984
+ children: [/* @__PURE__ */ jsx("span", {
985
+ className: "um-card__name",
986
+ title: item.name,
987
+ children: item.name
988
+ }), /* @__PURE__ */ jsx("span", {
989
+ className: "um-card__size",
990
+ children: formatBytes(item.size)
991
+ })]
992
+ }),
993
+ showProgress ? /* @__PURE__ */ jsx(UploadProgress, {
994
+ value: item.status === "success" ? 1 : item.progress,
995
+ status: item.status,
996
+ variant: progressVariant,
997
+ label: item.name,
998
+ valueText: progressValueText(item, labels),
999
+ size: 22
1000
+ }) : null,
1001
+ /* @__PURE__ */ jsx(ItemStatus, { item })
1002
+ ]
1003
+ }),
1004
+ /* @__PURE__ */ jsx(ItemActions, {
1005
+ item,
1006
+ allowPause
1007
+ }),
1008
+ celebrate && justSucceeded ? /* @__PURE__ */ jsx(Burst, {
1009
+ seed: item.id,
1010
+ count: 10,
1011
+ spread: 44,
1012
+ className: "um-card__burst"
1013
+ }) : null
1014
+ ]
1015
+ });
1016
+ });
1017
+ //#endregion
1018
+ //#region src/components/FileList.tsx
1019
+ /**
1020
+ * List with enter/exit presence and FLIP re-layout animations.
1021
+ * Generic so presets can animate any collection, not only upload items.
1022
+ */
1023
+ function AnimatedList({ items, renderItem, className, itemClassName, exitDuration = 320, as: Tag = "ul", layout, "aria-label": ariaLabel }) {
1024
+ const { motion } = useUploaderContext();
1025
+ const ref = useRef(null);
1026
+ const entries = usePresence(items, motion.reduced ? 0 : exitDuration);
1027
+ useFlip(ref, entries.map((entry) => `${entry.item.id}${entry.exiting ? "~" : ""}`).join("|"), !motion.reduced);
1028
+ const ItemTag = Tag === "div" ? "div" : "li";
1029
+ return /* @__PURE__ */ jsx(Tag, {
1030
+ ref,
1031
+ className: cx("um-list", className),
1032
+ "data-layout": layout,
1033
+ "aria-label": ariaLabel,
1034
+ children: entries.map((entry, index) => /* @__PURE__ */ jsx(ItemTag, {
1035
+ "data-flip-id": entry.item.id,
1036
+ className: cx("um-list__item", itemClassName),
1037
+ "data-exiting": entry.exiting || void 0,
1038
+ "aria-hidden": entry.exiting || void 0,
1039
+ style: { "--um-i": index },
1040
+ children: renderItem(entry.item, {
1041
+ exiting: entry.exiting,
1042
+ index
1043
+ })
1044
+ }, entry.item.id))
1045
+ });
1046
+ }
1047
+ /** The queue, rendered with `FileCard` (or your `renderFile`). */
1048
+ function UploadFileList({ layout = "list", progressVariant, renderFile, items, className, itemClassName, allowPause }) {
1049
+ const context = useUploaderContext();
1050
+ const list = items ?? context.items;
1051
+ if (list.length === 0) return null;
1052
+ return /* @__PURE__ */ jsx(AnimatedList, {
1053
+ items: list,
1054
+ className: cx("um-file-list", className),
1055
+ itemClassName,
1056
+ "aria-label": "Files",
1057
+ renderItem: (item) => renderFile ? renderFile(item, getFileHelpers(context, item)) : /* @__PURE__ */ jsx(FileCard, {
1058
+ item,
1059
+ progressVariant,
1060
+ allowPause
1061
+ }),
1062
+ layout
1063
+ });
1064
+ }
1065
+ //#endregion
1066
+ //#region src/components/Status.tsx
1067
+ /** "Uploading 3 files" + "63% · 6.4 MB/s · 5 seconds left". */
1068
+ function useSummaryText() {
1069
+ const { totals, labels } = useUploaderContext();
1070
+ const title = labels.summary(totals);
1071
+ const parts = [];
1072
+ if (totals.active > 0 || totals.paused > 0) {
1073
+ parts.push(formatPercent(totals.progress));
1074
+ if (totals.speed > 0) parts.push(formatSpeed(totals.speed));
1075
+ if (totals.eta !== null && totals.eta > .5) parts.push(labels.timeLeft(formatDuration(totals.eta)));
1076
+ }
1077
+ return {
1078
+ title,
1079
+ detail: parts.join(" · ")
1080
+ };
1081
+ }
1082
+ function UploadSummary({ className }) {
1083
+ const { title, detail } = useSummaryText();
1084
+ return /* @__PURE__ */ jsxs("div", {
1085
+ className: cx("um-summary", className),
1086
+ children: [/* @__PURE__ */ jsx("span", {
1087
+ className: "um-summary__title",
1088
+ children: title
1089
+ }), detail ? /* @__PURE__ */ jsx("span", {
1090
+ className: "um-summary__detail",
1091
+ children: detail
1092
+ }) : null]
1093
+ });
1094
+ }
1095
+ /** Badge shown when the transport is simulated, so demos never pass fake progress off as real. */
1096
+ function TransportBadge({ className }) {
1097
+ const { transport, labels } = useUploaderContext();
1098
+ if (transport !== "simulated") return null;
1099
+ return /* @__PURE__ */ jsxs("span", {
1100
+ className: cx("um-badge", className),
1101
+ title: labels.simulatedHint,
1102
+ children: [
1103
+ /* @__PURE__ */ jsx("span", {
1104
+ className: "um-badge__dot",
1105
+ "aria-hidden": "true"
1106
+ }),
1107
+ labels.simulated,
1108
+ /* @__PURE__ */ jsxs("span", {
1109
+ className: "um-sr-only",
1110
+ children: [": ", labels.simulatedHint]
1111
+ })
1112
+ ]
1113
+ });
1114
+ }
1115
+ /** Dismissible list of files that failed validation, with the reason in text (not colour alone). */
1116
+ function UploadRejections({ className }) {
1117
+ const { rejections, dismissRejection, labels } = useUploaderContext();
1118
+ if (rejections.length === 0) return null;
1119
+ return /* @__PURE__ */ jsx(AnimatedList, {
1120
+ items: rejections,
1121
+ className: cx("um-rejections", className),
1122
+ "aria-label": "Rejected files",
1123
+ renderItem: (rejection) => /* @__PURE__ */ jsxs("div", {
1124
+ className: "um-rejection",
1125
+ children: [
1126
+ /* @__PURE__ */ jsx(AlertIcon, { size: 16 }),
1127
+ /* @__PURE__ */ jsx("span", {
1128
+ className: "um-rejection__text",
1129
+ children: rejection.errors.map((error) => error.message).join(" ")
1130
+ }),
1131
+ /* @__PURE__ */ jsx("button", {
1132
+ type: "button",
1133
+ className: "um-icon-btn",
1134
+ "aria-label": `${labels.dismiss}: ${rejection.file.name}`,
1135
+ onClick: (event) => {
1136
+ event.stopPropagation();
1137
+ dismissRejection(rejection.id);
1138
+ },
1139
+ children: /* @__PURE__ */ jsx(CloseIcon, { size: 14 })
1140
+ })
1141
+ ]
1142
+ })
1143
+ });
1144
+ }
1145
+ //#endregion
1146
+ //#region src/animations/shared.tsx
1147
+ /** Rejections + queue, honouring `renderFile`, `classNames` and `showRejections`. */
1148
+ function PresetQueue({ props, progressVariant, allowPause, items, className }) {
1149
+ return /* @__PURE__ */ jsxs(Fragment, { children: [props.showRejections !== false ? /* @__PURE__ */ jsx(UploadRejections, {}) : null, /* @__PURE__ */ jsx(UploadFileList, {
1150
+ className: [
1151
+ className,
1152
+ "um-queue",
1153
+ props.classNames?.list
1154
+ ].filter(Boolean).join(" "),
1155
+ itemClassName: props.classNames?.item,
1156
+ items,
1157
+ layout: props.layout,
1158
+ progressVariant: props.progressVariant ?? progressVariant,
1159
+ renderFile: props.renderFile,
1160
+ allowPause
1161
+ })] });
1162
+ }
1163
+ /** Renders `props.renderFile` when provided, otherwise the preset's own visual. */
1164
+ function useRenderFile(props) {
1165
+ const context = useUploaderContext();
1166
+ return (item, fallback) => props.renderFile ? props.renderFile(item, getFileHelpers(context, item)) : fallback();
1167
+ }
1168
+ const KIND_COLORS = {
1169
+ pdf: "#e5484d",
1170
+ document: "#3b82f6",
1171
+ spreadsheet: "#16a34a",
1172
+ presentation: "#f97316",
1173
+ image: "#8b5cf6",
1174
+ video: "#ec4899",
1175
+ audio: "#f59e0b",
1176
+ archive: "#a16207",
1177
+ code: "#0ea5e9",
1178
+ data: "#ca8a04",
1179
+ text: "#64748b",
1180
+ unknown: "#94a3b8"
1181
+ };
1182
+ //#endregion
1183
+ //#region src/animations/beam.tsx
1184
+ function BeamPreset(props) {
1185
+ const { dropzone, labels, totals } = useUploaderContext();
1186
+ const mode = dropzone.dragState !== "idle" ? dropzone.dragState : totals.active > 0 ? "active" : "idle";
1187
+ return /* @__PURE__ */ jsxs("div", {
1188
+ className: cx("um-beam", props.classNames?.root),
1189
+ "data-mode": mode,
1190
+ children: [
1191
+ /* @__PURE__ */ jsxs("div", {
1192
+ ...dropzone.getRootProps(),
1193
+ className: cx("um-zone", "um-beam__zone", props.classNames?.zone),
1194
+ children: [
1195
+ /* @__PURE__ */ jsx("span", {
1196
+ className: "um-beam__border",
1197
+ "aria-hidden": "true"
1198
+ }),
1199
+ /* @__PURE__ */ jsx("span", {
1200
+ className: "um-beam__grid um-deco",
1201
+ "aria-hidden": "true"
1202
+ }),
1203
+ props.zoneContent ?? /* @__PURE__ */ jsxs("div", {
1204
+ className: "um-beam__content",
1205
+ children: [
1206
+ /* @__PURE__ */ jsx("span", {
1207
+ className: "um-beam__icon",
1208
+ children: /* @__PURE__ */ jsx(CloudIcon, { size: 26 })
1209
+ }),
1210
+ /* @__PURE__ */ jsx("strong", {
1211
+ className: "um-beam__title",
1212
+ children: dropzone.dragState === "reject" ? labels.dropReject : dropzone.dragState === "accept" ? labels.dropActive : props.title ?? "Drop files to sync"
1213
+ }),
1214
+ /* @__PURE__ */ jsx("span", {
1215
+ className: "um-beam__sub",
1216
+ children: /* @__PURE__ */ jsx(BrowseButton, {
1217
+ variant: "soft",
1218
+ children: labels.browse
1219
+ })
1220
+ }),
1221
+ /* @__PURE__ */ jsx(UploadHint, {})
1222
+ ]
1223
+ })
1224
+ ]
1225
+ }),
1226
+ /* @__PURE__ */ jsx(PresetQueue, {
1227
+ props,
1228
+ progressVariant: "beam",
1229
+ allowPause: true
1230
+ }),
1231
+ /* @__PURE__ */ jsx(TransportBadge, { className: "um-beam__badge" })
1232
+ ]
1233
+ });
1234
+ }
1235
+ const beamPreset = {
1236
+ ...PRESET_INFO["beam"],
1237
+ Component: BeamPreset
1238
+ };
1239
+ //#endregion
1240
+ //#region src/animations/card-fan.tsx
1241
+ function CurvedArrow({ side }) {
1242
+ return /* @__PURE__ */ jsxs("svg", {
1243
+ className: "um-fan__arrow",
1244
+ "data-side": side,
1245
+ width: "44",
1246
+ height: "44",
1247
+ viewBox: "0 0 44 44",
1248
+ "aria-hidden": "true",
1249
+ children: [/* @__PURE__ */ jsx("path", {
1250
+ className: "um-fan__arrow-line",
1251
+ d: "M8 4 C 4 18, 12 30, 32 31",
1252
+ pathLength: 1
1253
+ }), /* @__PURE__ */ jsx("path", {
1254
+ className: "um-fan__arrow-head",
1255
+ d: "M24 24 L 33 31 L 24 38",
1256
+ pathLength: 1
1257
+ })]
1258
+ });
1259
+ }
1260
+ function CardFanPreset(props) {
1261
+ const { dropzone, labels, totals, autoUpload, transport } = useUploaderContext();
1262
+ const { dragState, isWindowDrag, lastDrop } = dropzone;
1263
+ const mode = dragState === "reject" ? "reject" : dragState === "accept" ? "over" : isWindowDrag ? "anticipate" : "idle";
1264
+ const showStart = !autoUpload || totals.pending > 0;
1265
+ const headline = mode === "reject" ? labels.dropReject : mode === "over" ? labels.dropActive : props.title ? props.title : /* @__PURE__ */ jsxs(Fragment, { children: [
1266
+ "Drag & drop ",
1267
+ /* @__PURE__ */ jsx("em", { children: "images," }),
1268
+ " ",
1269
+ /* @__PURE__ */ jsx("em", { children: "videos," }),
1270
+ " or any ",
1271
+ /* @__PURE__ */ jsx("em", { children: "file" })
1272
+ ] });
1273
+ return /* @__PURE__ */ jsxs("div", {
1274
+ className: cx("um-panel", "um-fan", props.classNames?.root),
1275
+ "data-mode": mode,
1276
+ children: [
1277
+ /* @__PURE__ */ jsxs("div", {
1278
+ ...dropzone.getRootProps(),
1279
+ className: cx("um-zone", "um-fan__zone", props.classNames?.zone),
1280
+ children: [/* @__PURE__ */ jsx("svg", {
1281
+ className: "um-fan__border",
1282
+ "aria-hidden": "true",
1283
+ children: /* @__PURE__ */ jsx("rect", {
1284
+ x: "0",
1285
+ y: "0",
1286
+ width: "100%",
1287
+ height: "100%",
1288
+ rx: "16",
1289
+ ry: "16"
1290
+ })
1291
+ }), props.zoneContent ?? /* @__PURE__ */ jsxs("div", {
1292
+ className: "um-fan__content",
1293
+ children: [
1294
+ /* @__PURE__ */ jsxs("div", {
1295
+ className: "um-fan__stage",
1296
+ "aria-hidden": "true",
1297
+ children: [
1298
+ /* @__PURE__ */ jsx(CurvedArrow, { side: "left" }),
1299
+ /* @__PURE__ */ jsxs("div", {
1300
+ className: "um-fan__cards",
1301
+ "data-dealt": lastDrop ? "true" : void 0,
1302
+ children: [
1303
+ /* @__PURE__ */ jsx("span", {
1304
+ className: "um-fan__card",
1305
+ "data-pos": "left",
1306
+ children: /* @__PURE__ */ jsx(VideoIcon, { size: 20 })
1307
+ }),
1308
+ /* @__PURE__ */ jsx("span", {
1309
+ className: "um-fan__card",
1310
+ "data-pos": "center",
1311
+ children: /* @__PURE__ */ jsx(ImageIcon, { size: 24 })
1312
+ }),
1313
+ /* @__PURE__ */ jsx("span", {
1314
+ className: "um-fan__card",
1315
+ "data-pos": "right",
1316
+ children: /* @__PURE__ */ jsx(MusicIcon, { size: 20 })
1317
+ })
1318
+ ]
1319
+ }, lastDrop?.at ?? 0),
1320
+ /* @__PURE__ */ jsx(CurvedArrow, { side: "right" })
1321
+ ]
1322
+ }),
1323
+ /* @__PURE__ */ jsx("p", {
1324
+ className: "um-fan__title",
1325
+ "aria-live": "polite",
1326
+ children: headline
1327
+ }),
1328
+ /* @__PURE__ */ jsxs("p", {
1329
+ className: "um-fan__sub",
1330
+ children: [
1331
+ "or",
1332
+ " ",
1333
+ /* @__PURE__ */ jsx(BrowseButton, {
1334
+ variant: "link",
1335
+ className: "um-fan__browse",
1336
+ children: "browse files"
1337
+ }),
1338
+ " ",
1339
+ "on your computer"
1340
+ ]
1341
+ }),
1342
+ /* @__PURE__ */ jsx(UploadHint, { className: "um-fan__hint" })
1343
+ ]
1344
+ })]
1345
+ }),
1346
+ /* @__PURE__ */ jsx(PresetQueue, {
1347
+ props,
1348
+ progressVariant: "bar"
1349
+ }),
1350
+ showStart || transport === "simulated" ? /* @__PURE__ */ jsxs("div", {
1351
+ className: "um-fan__footer",
1352
+ children: [/* @__PURE__ */ jsx(TransportBadge, {}), showStart ? /* @__PURE__ */ jsx(StartUploadButton, {}) : null]
1353
+ }) : null
1354
+ ]
1355
+ });
1356
+ }
1357
+ const cardFanPreset = {
1358
+ ...PRESET_INFO["card-fan"],
1359
+ Component: CardFanPreset
1360
+ };
1361
+ //#endregion
1362
+ //#region src/animations/essential.tsx
1363
+ function EssentialPreset(props) {
1364
+ const { dropzone, autoUpload, totals, transport } = useUploaderContext();
1365
+ const showStart = !autoUpload && totals.pending + totals.active > 0;
1366
+ return /* @__PURE__ */ jsxs("div", {
1367
+ className: cx("um-essential", props.classNames?.root),
1368
+ children: [
1369
+ /* @__PURE__ */ jsx("div", {
1370
+ ...dropzone.getRootProps(),
1371
+ className: cx("um-zone", "um-essential__zone", props.classNames?.zone),
1372
+ children: props.zoneContent ?? /* @__PURE__ */ jsx(DropzoneContent, { title: props.title })
1373
+ }),
1374
+ /* @__PURE__ */ jsx(PresetQueue, {
1375
+ props,
1376
+ progressVariant: "bar",
1377
+ allowPause: true
1378
+ }),
1379
+ showStart || transport === "simulated" ? /* @__PURE__ */ jsxs("div", {
1380
+ className: "um-footer-row",
1381
+ children: [/* @__PURE__ */ jsx(TransportBadge, {}), showStart ? /* @__PURE__ */ jsx(StartUploadButton, {}) : null]
1382
+ }) : null
1383
+ ]
1384
+ });
1385
+ }
1386
+ const essentialPreset = {
1387
+ ...PRESET_INFO["essential"],
1388
+ Component: EssentialPreset
1389
+ };
1390
+ //#endregion
1391
+ //#region src/animations/hoop-shot.tsx
1392
+ const GRAVITY = 2200;
1393
+ const POWER = 10.5;
1394
+ const MAX_PULL = 120;
1395
+ const ASSIST = 16;
1396
+ const TOKEN_W = 44;
1397
+ const TOKEN_H = 54;
1398
+ function relativeRect(element, container) {
1399
+ const a = element.getBoundingClientRect();
1400
+ const b = container.getBoundingClientRect();
1401
+ return {
1402
+ left: a.left - b.left,
1403
+ top: a.top - b.top,
1404
+ right: a.right - b.left,
1405
+ bottom: a.bottom - b.top,
1406
+ width: a.width,
1407
+ height: a.height
1408
+ };
1409
+ }
1410
+ /** Velocity that lands exactly in the rim centre after `time` seconds. */
1411
+ function perfectVelocity(from, geometry, time = .85) {
1412
+ const target = {
1413
+ x: (geometry.rimLeft + geometry.rimRight) / 2,
1414
+ y: geometry.rimY - 2
1415
+ };
1416
+ return {
1417
+ x: (target.x - from.x) / time,
1418
+ y: (target.y - from.y - .5 * GRAVITY * time * time) / time
1419
+ };
1420
+ }
1421
+ function trajectory(from, velocity, steps = 18, dt = .045) {
1422
+ return Array.from({ length: steps }, (_, index) => {
1423
+ const t = (index + 1) * dt;
1424
+ return {
1425
+ x: from.x + velocity.x * t,
1426
+ y: from.y + velocity.y * t + .5 * GRAVITY * t * t
1427
+ };
1428
+ });
1429
+ }
1430
+ function Token({ item }) {
1431
+ return /* @__PURE__ */ jsxs("span", {
1432
+ className: "um-hoop__token-face",
1433
+ children: [/* @__PURE__ */ jsx("span", { className: "um-hoop__token-lines" }), /* @__PURE__ */ jsx(FileIcon, {
1434
+ kind: item.kind,
1435
+ label: getFileLabel(item),
1436
+ size: 30
1437
+ })]
1438
+ });
1439
+ }
1440
+ function Hoop({ netKey, rimRef, boardRef }) {
1441
+ const net = Array.from({ length: 7 }, (_, i) => i);
1442
+ return /* @__PURE__ */ jsxs("div", {
1443
+ className: "um-hoop__hoop",
1444
+ "aria-hidden": "true",
1445
+ children: [
1446
+ /* @__PURE__ */ jsx("span", {
1447
+ className: "um-hoop__board",
1448
+ ref: boardRef,
1449
+ children: /* @__PURE__ */ jsx("span", { className: "um-hoop__board-square" })
1450
+ }),
1451
+ /* @__PURE__ */ jsxs("svg", {
1452
+ className: "um-hoop__rim-svg",
1453
+ viewBox: "0 0 96 90",
1454
+ width: "96",
1455
+ height: "90",
1456
+ children: [
1457
+ /* @__PURE__ */ jsxs("g", {
1458
+ className: "um-hoop__net",
1459
+ "data-swish": netKey > 0 || void 0,
1460
+ children: [net.map((i) => /* @__PURE__ */ jsx("path", { d: `M${10 + i * 12.6} 12 L ${26 + i * 7.4} 80` }, `v${i}`)), /* @__PURE__ */ jsx("path", { d: "M16 30 Q 48 38 80 30 M20 48 Q 48 55 76 48 M24 66 Q 48 72 72 66 M26 80 L 70 80" })]
1461
+ }, netKey),
1462
+ /* @__PURE__ */ jsx("ellipse", {
1463
+ className: "um-hoop__rim",
1464
+ cx: "48",
1465
+ cy: "12",
1466
+ rx: "40",
1467
+ ry: "7"
1468
+ }),
1469
+ /* @__PURE__ */ jsx("rect", {
1470
+ className: "um-hoop__clip",
1471
+ x: "40",
1472
+ y: "2",
1473
+ width: "16",
1474
+ height: "6",
1475
+ rx: "2"
1476
+ })
1477
+ ]
1478
+ }),
1479
+ /* @__PURE__ */ jsx("span", {
1480
+ className: "um-hoop__rim-target",
1481
+ ref: rimRef
1482
+ })
1483
+ ]
1484
+ });
1485
+ }
1486
+ function HoopShotPreset(props) {
1487
+ const context = useUploaderContext();
1488
+ const { items, dropzone, labels, motion, start, remove, totals } = context;
1489
+ const courtRef = useRef(null);
1490
+ const rimRef = useRef(null);
1491
+ const boardRef = useRef(null);
1492
+ const ballRef = useRef(null);
1493
+ const frame = useRef(null);
1494
+ const autoQueue = useRef([]);
1495
+ const aimState = useRef(null);
1496
+ const [flight, setFlight] = useState(null);
1497
+ const [aim, setAim] = useState(null);
1498
+ const [netKey, setNetKey] = useState(0);
1499
+ const [callout, setCallout] = useState(null);
1500
+ const [stats, setStats] = useState({
1501
+ shots: 0,
1502
+ made: 0
1503
+ });
1504
+ const pending = items.filter((item) => item.status === "pending" && item.id !== flight?.id);
1505
+ const top = pending[pending.length - 1];
1506
+ const launched = items.filter((item) => item.status !== "pending");
1507
+ useEffect(() => () => {
1508
+ if (frame.current !== null) cancelAnimationFrame(frame.current);
1509
+ }, []);
1510
+ const geometry = useCallback(() => {
1511
+ const court = courtRef.current;
1512
+ if (!court || !rimRef.current || !boardRef.current) return null;
1513
+ const rim = relativeRect(rimRef.current, court);
1514
+ const board = relativeRect(boardRef.current, court);
1515
+ const bounds = court.getBoundingClientRect();
1516
+ return {
1517
+ rimLeft: rim.left,
1518
+ rimRight: rim.right,
1519
+ rimY: rim.top,
1520
+ board: {
1521
+ left: board.left,
1522
+ right: board.right,
1523
+ top: board.top,
1524
+ bottom: board.bottom
1525
+ },
1526
+ width: bounds.width,
1527
+ height: bounds.height
1528
+ };
1529
+ }, []);
1530
+ const padOrigin = useCallback(() => {
1531
+ const court = courtRef.current;
1532
+ const pad = court?.querySelector(".um-hoop__pad");
1533
+ if (!court || !pad) return null;
1534
+ const rect = relativeRect(pad, court);
1535
+ return {
1536
+ x: rect.left + TOKEN_W / 2,
1537
+ y: rect.bottom - TOKEN_H / 2
1538
+ };
1539
+ }, []);
1540
+ const score = useCallback((item, kind) => {
1541
+ setStats((s) => ({
1542
+ shots: s.shots + 1,
1543
+ made: s.made + (kind === "swish" || kind === "bank" ? 1 : 0)
1544
+ }));
1545
+ if (kind === "swish" || kind === "bank") {
1546
+ setNetKey((k) => k + 1);
1547
+ setCallout({
1548
+ text: kind === "bank" ? "Bank shot!" : "Swish!",
1549
+ key: Date.now(),
1550
+ tone: "good"
1551
+ });
1552
+ start(item.id);
1553
+ } else setCallout({
1554
+ text: kind === "rim" ? "Rim out – try again" : "Missed – try again",
1555
+ key: Date.now(),
1556
+ tone: "bad"
1557
+ });
1558
+ }, [start]);
1559
+ const shootNextQueued = useRef(() => {});
1560
+ const launch = useCallback((item, from, velocity) => {
1561
+ const g = geometry();
1562
+ if (!g || motion.reduced) {
1563
+ score(item, "swish");
1564
+ setTimeout(() => shootNextQueued.current(), 0);
1565
+ return;
1566
+ }
1567
+ setFlight(item);
1568
+ let position = { ...from };
1569
+ let v = { ...velocity };
1570
+ let rotation = 0;
1571
+ let last = performance.now();
1572
+ let banked = false;
1573
+ let sinking = false;
1574
+ let sinkFrames = 0;
1575
+ const place = (scale = 1, opacity = 1) => {
1576
+ const ball = ballRef.current;
1577
+ if (!ball) return;
1578
+ ball.style.transform = `translate(${position.x - TOKEN_W / 2}px, ${position.y - TOKEN_H / 2}px) rotate(${rotation}deg) scale(${scale})`;
1579
+ ball.style.opacity = String(opacity);
1580
+ };
1581
+ const finish = (result) => {
1582
+ frame.current = null;
1583
+ score(item, result);
1584
+ setFlight(null);
1585
+ setTimeout(() => shootNextQueued.current(), result === "miss" || result === "rim" ? 500 : 180);
1586
+ };
1587
+ const step = (now) => {
1588
+ const dt = Math.min((now - last) / 1e3, 1 / 30);
1589
+ last = now;
1590
+ const previousY = position.y;
1591
+ v.y += GRAVITY * dt;
1592
+ position = {
1593
+ x: position.x + v.x * dt,
1594
+ y: position.y + v.y * dt
1595
+ };
1596
+ rotation += v.x * dt * .45;
1597
+ if (sinking) {
1598
+ sinkFrames += 1;
1599
+ position.x += ((g.rimLeft + g.rimRight) / 2 - position.x) * .25;
1600
+ place(Math.max(.2, 1 - sinkFrames / 14), Math.max(0, 1 - sinkFrames / 16));
1601
+ if (sinkFrames > 16) return finish(banked ? "bank" : "swish");
1602
+ frame.current = requestAnimationFrame(step);
1603
+ return;
1604
+ }
1605
+ const b = g.board;
1606
+ if (!banked && position.x > b.left && position.x < b.right && position.y > b.top && position.y < b.bottom) {
1607
+ banked = true;
1608
+ v = {
1609
+ x: v.x * .15,
1610
+ y: Math.max(v.y, 0) * .4
1611
+ };
1612
+ }
1613
+ if (previousY < g.rimY && position.y >= g.rimY && v.y > 0) {
1614
+ const inside = position.x > g.rimLeft + 6 && position.x < g.rimRight - 6;
1615
+ const assisted = position.x > g.rimLeft - ASSIST && position.x < g.rimRight + ASSIST;
1616
+ const nearRim = position.x > g.rimLeft - ASSIST - 22 && position.x < g.rimRight + ASSIST + 22;
1617
+ if (inside || assisted) {
1618
+ sinking = true;
1619
+ v = {
1620
+ x: 0,
1621
+ y: 260
1622
+ };
1623
+ } else if (nearRim) {
1624
+ const center = (g.rimLeft + g.rimRight) / 2;
1625
+ v = {
1626
+ x: (position.x < center ? -1 : 1) * 260,
1627
+ y: -Math.abs(v.y) * .35
1628
+ };
1629
+ position.y = g.rimY - 1;
1630
+ setTimeout(() => setCallout({
1631
+ text: "Clank!",
1632
+ key: Date.now(),
1633
+ tone: "bad"
1634
+ }), 0);
1635
+ }
1636
+ }
1637
+ place();
1638
+ if (position.y > g.height + 80 || position.x < -80 || position.x > g.width + 80) return finish(banked ? "rim" : "miss");
1639
+ frame.current = requestAnimationFrame(step);
1640
+ };
1641
+ place();
1642
+ frame.current = requestAnimationFrame(step);
1643
+ }, [
1644
+ geometry,
1645
+ motion.reduced,
1646
+ score
1647
+ ]);
1648
+ const autoShoot = useCallback((item) => {
1649
+ const g = geometry();
1650
+ const origin = padOrigin();
1651
+ if (!g || !origin) {
1652
+ start(item.id);
1653
+ return;
1654
+ }
1655
+ launch(item, origin, perfectVelocity(origin, g));
1656
+ }, [
1657
+ geometry,
1658
+ padOrigin,
1659
+ launch,
1660
+ start
1661
+ ]);
1662
+ shootNextQueued.current = () => {
1663
+ const nextId = autoQueue.current.shift();
1664
+ if (!nextId) return;
1665
+ const next = context.store.getItem(nextId);
1666
+ if (next && next.status === "pending") autoShoot(next);
1667
+ else shootNextQueued.current();
1668
+ };
1669
+ const shootAll = () => {
1670
+ if (flight) return;
1671
+ autoQueue.current = [...pending].reverse().map((item) => item.id);
1672
+ shootNextQueued.current();
1673
+ };
1674
+ const onPointerDown = (event) => {
1675
+ if (!top || flight || event.button !== 0) return;
1676
+ const origin = padOrigin();
1677
+ if (!origin) return;
1678
+ event.currentTarget.setPointerCapture(event.pointerId);
1679
+ aimState.current = {
1680
+ pointerId: event.pointerId,
1681
+ origin,
1682
+ offset: {
1683
+ x: 0,
1684
+ y: 0
1685
+ },
1686
+ item: top
1687
+ };
1688
+ setAim({
1689
+ origin,
1690
+ offset: {
1691
+ x: 0,
1692
+ y: 0
1693
+ }
1694
+ });
1695
+ event.currentTarget.dataset.start = `${event.clientX},${event.clientY}`;
1696
+ };
1697
+ const onPointerMove = (event) => {
1698
+ const state = aimState.current;
1699
+ if (!state || state.pointerId !== event.pointerId) return;
1700
+ const [sx, sy] = (event.currentTarget.dataset.start ?? "0,0").split(",").map(Number);
1701
+ let dx = event.clientX - sx;
1702
+ let dy = event.clientY - sy;
1703
+ const length = Math.hypot(dx, dy);
1704
+ if (length > MAX_PULL) {
1705
+ dx = dx / length * MAX_PULL;
1706
+ dy = dy / length * MAX_PULL;
1707
+ }
1708
+ state.offset = {
1709
+ x: dx,
1710
+ y: dy
1711
+ };
1712
+ setAim({
1713
+ origin: state.origin,
1714
+ offset: state.offset
1715
+ });
1716
+ };
1717
+ const onPointerUp = (event) => {
1718
+ const state = aimState.current;
1719
+ if (!state || state.pointerId !== event.pointerId) return;
1720
+ aimState.current = null;
1721
+ setAim(null);
1722
+ const { origin, offset, item } = state;
1723
+ if (Math.hypot(offset.x, offset.y) < 12) {
1724
+ autoShoot(item);
1725
+ return;
1726
+ }
1727
+ const from = {
1728
+ x: origin.x + offset.x,
1729
+ y: origin.y + offset.y
1730
+ };
1731
+ launch(item, from, {
1732
+ x: -offset.x * POWER,
1733
+ y: -offset.y * POWER
1734
+ });
1735
+ };
1736
+ const aimFrom = aim ? {
1737
+ x: aim.origin.x + aim.offset.x,
1738
+ y: aim.origin.y + aim.offset.y
1739
+ } : null;
1740
+ const dots = aim && aimFrom ? trajectory(aimFrom, {
1741
+ x: -aim.offset.x * POWER,
1742
+ y: -aim.offset.y * POWER
1743
+ }) : [];
1744
+ const uploaded = totals.success;
1745
+ return /* @__PURE__ */ jsxs("div", {
1746
+ className: cx("um-panel", "um-hoop", props.classNames?.root),
1747
+ children: [
1748
+ /* @__PURE__ */ jsxs("div", {
1749
+ className: "um-panel__header",
1750
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
1751
+ className: "um-panel__title",
1752
+ children: props.title ?? "Upload files"
1753
+ }), /* @__PURE__ */ jsx("p", {
1754
+ className: "um-panel__desc",
1755
+ children: props.description ?? "Drag and drop, or take the shot."
1756
+ })] }), /* @__PURE__ */ jsxs("div", {
1757
+ className: "um-hoop__pills",
1758
+ children: [
1759
+ /* @__PURE__ */ jsx(TransportBadge, {}),
1760
+ stats.shots > 0 ? /* @__PURE__ */ jsxs("span", {
1761
+ className: "um-hoop__pill",
1762
+ title: "Shots made",
1763
+ children: [
1764
+ "🏀 ",
1765
+ stats.made,
1766
+ "/",
1767
+ stats.shots
1768
+ ]
1769
+ }) : null,
1770
+ /* @__PURE__ */ jsxs("span", {
1771
+ className: "um-hoop__pill um-hoop__pill--accent",
1772
+ "data-bump": uploaded > 0 || void 0,
1773
+ children: ["Uploaded ", /* @__PURE__ */ jsx("strong", { children: uploaded })]
1774
+ }, uploaded)
1775
+ ]
1776
+ })]
1777
+ }),
1778
+ /* @__PURE__ */ jsxs("div", {
1779
+ ...dropzone.getRootProps({ noClick: Boolean(top) }),
1780
+ ref: courtRef,
1781
+ className: cx("um-zone", "um-hoop__court", props.classNames?.zone),
1782
+ "data-aiming": aim ? "true" : void 0,
1783
+ children: [
1784
+ /* @__PURE__ */ jsx("div", {
1785
+ className: "um-hoop__drop",
1786
+ children: props.zoneContent ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1787
+ /* @__PURE__ */ jsx(UploadIcon, { size: 18 }),
1788
+ /* @__PURE__ */ jsx("strong", { children: dropzone.dragState === "accept" ? labels.dropActive : dropzone.dragState === "reject" ? labels.dropReject : "Drop files here" }),
1789
+ /* @__PURE__ */ jsxs("span", { children: ["or take the shot · ", /* @__PURE__ */ jsx(BrowseButton, {
1790
+ variant: "link",
1791
+ children: "browse"
1792
+ })] }),
1793
+ /* @__PURE__ */ jsx(UploadHint, {})
1794
+ ] })
1795
+ }),
1796
+ /* @__PURE__ */ jsx(Hoop, {
1797
+ netKey,
1798
+ rimRef,
1799
+ boardRef
1800
+ }),
1801
+ netKey > 0 ? /* @__PURE__ */ jsx(Burst, {
1802
+ seed: `hoop${netKey}`,
1803
+ count: 16,
1804
+ spread: 64,
1805
+ className: "um-hoop__burst"
1806
+ }, netKey) : null,
1807
+ callout ? /* @__PURE__ */ jsx("span", {
1808
+ className: "um-hoop__callout",
1809
+ "data-tone": callout.tone,
1810
+ "aria-hidden": "true",
1811
+ children: callout.text
1812
+ }, callout.key) : null,
1813
+ dots.length > 0 ? /* @__PURE__ */ jsx("svg", {
1814
+ className: "um-hoop__dots",
1815
+ "aria-hidden": "true",
1816
+ children: dots.map((dot, index) => /* @__PURE__ */ jsx("circle", {
1817
+ cx: dot.x,
1818
+ cy: dot.y,
1819
+ r: 2.4 - index * .06,
1820
+ style: { opacity: 1 - index / dots.length }
1821
+ }, index))
1822
+ }) : null,
1823
+ /* @__PURE__ */ jsxs("div", {
1824
+ className: "um-hoop__pad",
1825
+ "data-count": pending.length,
1826
+ children: [
1827
+ pending.slice(-3, -1).map((item, index) => /* @__PURE__ */ jsx("span", {
1828
+ className: "um-hoop__token um-hoop__token--stacked",
1829
+ style: { "--d": index },
1830
+ "aria-hidden": "true",
1831
+ children: /* @__PURE__ */ jsx(Token, { item })
1832
+ }, item.id)),
1833
+ top ? /* @__PURE__ */ jsx("button", {
1834
+ type: "button",
1835
+ className: "um-hoop__token um-hoop__token--top",
1836
+ "aria-label": `Shoot ${top.name} into the basket to upload it`,
1837
+ onPointerDown,
1838
+ onPointerMove,
1839
+ onPointerUp,
1840
+ onPointerCancel: () => {
1841
+ aimState.current = null;
1842
+ setAim(null);
1843
+ },
1844
+ onKeyDown: (event) => {
1845
+ if (event.key === "Enter" || event.key === " ") {
1846
+ event.preventDefault();
1847
+ if (!flight) autoShoot(top);
1848
+ }
1849
+ },
1850
+ onClick: (event) => event.stopPropagation(),
1851
+ style: aim ? { transform: `translate(${aim.offset.x}px, ${aim.offset.y}px) rotate(${aim.offset.x * -.2}deg)` } : void 0,
1852
+ children: /* @__PURE__ */ jsx(Token, { item: top })
1853
+ }) : /* @__PURE__ */ jsx("span", {
1854
+ className: "um-hoop__token um-hoop__token--empty",
1855
+ "aria-hidden": "true"
1856
+ }),
1857
+ pending.length > 1 ? /* @__PURE__ */ jsxs("span", {
1858
+ className: "um-hoop__count",
1859
+ children: ["×", pending.length]
1860
+ }) : null
1861
+ ]
1862
+ }),
1863
+ flight ? /* @__PURE__ */ jsx("div", {
1864
+ className: "um-hoop__ball",
1865
+ ref: ballRef,
1866
+ "aria-hidden": "true",
1867
+ children: /* @__PURE__ */ jsx(Token, { item: flight })
1868
+ }) : null,
1869
+ /* @__PURE__ */ jsxs("div", {
1870
+ className: "um-hoop__footer",
1871
+ children: [/* @__PURE__ */ jsx("span", {
1872
+ className: "um-hoop__tip",
1873
+ children: top ? "Pull back the file, aim & release to shoot into the basket" : "Add files, then aim & shoot them into the basket"
1874
+ }), top ? /* @__PURE__ */ jsxs("span", {
1875
+ className: "um-hoop__actions",
1876
+ children: [/* @__PURE__ */ jsx("button", {
1877
+ type: "button",
1878
+ className: "um-icon-btn",
1879
+ "aria-label": `${labels.remove} ${top.name}`,
1880
+ title: `${labels.remove} ${top.name}`,
1881
+ onClick: (event) => {
1882
+ event.stopPropagation();
1883
+ remove(top.id);
1884
+ },
1885
+ children: /* @__PURE__ */ jsx(CloseIcon, { size: 14 })
1886
+ }), /* @__PURE__ */ jsxs("button", {
1887
+ type: "button",
1888
+ className: "um-btn um-btn--soft",
1889
+ disabled: Boolean(flight),
1890
+ onClick: (event) => {
1891
+ event.stopPropagation();
1892
+ shootAll();
1893
+ },
1894
+ children: ["Shoot ", pending.length > 1 ? `all ${pending.length}` : "it"]
1895
+ })]
1896
+ }) : null]
1897
+ })
1898
+ ]
1899
+ }),
1900
+ /* @__PURE__ */ jsx(PresetQueue, {
1901
+ props,
1902
+ progressVariant: "ring",
1903
+ items: launched
1904
+ })
1905
+ ]
1906
+ });
1907
+ }
1908
+ const hoopShotPreset = {
1909
+ ...PRESET_INFO["hoop-shot"],
1910
+ Component: HoopShotPreset
1911
+ };
1912
+ //#endregion
1913
+ //#region src/animations/liquid.tsx
1914
+ const FRONT = /* @__PURE__ */ wavePath(800, 200, 7, 10) + " V 20 H 0 Z";
1915
+ const BACK = /* @__PURE__ */ wavePath(800, 160, 9, 10) + " V 20 H 0 Z";
1916
+ function LiquidPreset(props) {
1917
+ const { dropzone, totals, items, labels } = useUploaderContext();
1918
+ const { title, detail } = useSummaryText();
1919
+ const hasFiles = items.length > 0;
1920
+ const done = totals.isComplete && totals.error === 0;
1921
+ const dragging = dropzone.dragState === "accept";
1922
+ const level = done ? 1 : hasFiles ? Math.max(.08, totals.progress) : dragging ? .24 : .1;
1923
+ const bubbles = useMemo(() => {
1924
+ const random = seededRandom("liquid");
1925
+ return Array.from({ length: 9 }, () => ({
1926
+ left: 6 + random() * 88,
1927
+ size: 4 + random() * 8,
1928
+ delay: random() * 3,
1929
+ duration: 2.2 + random() * 2
1930
+ }));
1931
+ }, []);
1932
+ const content = hasFiles ? /* @__PURE__ */ jsxs(Fragment, { children: [
1933
+ /* @__PURE__ */ jsx("span", {
1934
+ className: "um-liquid__percent",
1935
+ children: done ? /* @__PURE__ */ jsx(AnimatedCheck, { size: 40 }) : `${Math.round(totals.progress * 100)}%`
1936
+ }),
1937
+ /* @__PURE__ */ jsx("strong", {
1938
+ className: "um-liquid__title",
1939
+ children: title
1940
+ }),
1941
+ /* @__PURE__ */ jsx("span", {
1942
+ className: "um-liquid__detail",
1943
+ children: detail || (done ? "Drop more to refill" : labels.status[totals.error > 0 ? "error" : "pending"])
1944
+ })
1945
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1946
+ /* @__PURE__ */ jsx("span", {
1947
+ className: "um-liquid__icon",
1948
+ children: /* @__PURE__ */ jsx(UploadIcon, { size: 26 })
1949
+ }),
1950
+ /* @__PURE__ */ jsx("strong", {
1951
+ className: "um-liquid__title",
1952
+ children: dropzone.dragState === "reject" ? labels.dropReject : dragging ? labels.dropActive : props.title ?? "Pour your files in"
1953
+ }),
1954
+ /* @__PURE__ */ jsx("span", {
1955
+ className: "um-liquid__detail",
1956
+ children: "Drop files, paste, or press Enter to browse"
1957
+ })
1958
+ ] });
1959
+ return /* @__PURE__ */ jsxs("div", {
1960
+ className: cx("um-liquid", props.classNames?.root),
1961
+ "data-active": totals.active > 0 || void 0,
1962
+ "data-done": done || void 0,
1963
+ "data-error": totals.error > 0 && totals.active === 0 || void 0,
1964
+ children: [
1965
+ /* @__PURE__ */ jsxs("div", {
1966
+ ...dropzone.getRootProps({ focusable: true }),
1967
+ className: cx("um-zone", "um-liquid__vessel", props.classNames?.zone),
1968
+ style: { "--um-level": level },
1969
+ children: [/* @__PURE__ */ jsxs("div", {
1970
+ className: "um-liquid__fluid",
1971
+ "aria-hidden": "true",
1972
+ children: [
1973
+ /* @__PURE__ */ jsx("svg", {
1974
+ className: "um-liquid__wave um-liquid__wave--back",
1975
+ viewBox: "0 0 800 20",
1976
+ preserveAspectRatio: "none",
1977
+ children: /* @__PURE__ */ jsx("path", { d: BACK })
1978
+ }),
1979
+ /* @__PURE__ */ jsx("svg", {
1980
+ className: "um-liquid__wave um-liquid__wave--front",
1981
+ viewBox: "0 0 800 20",
1982
+ preserveAspectRatio: "none",
1983
+ children: /* @__PURE__ */ jsx("path", { d: FRONT })
1984
+ }),
1985
+ /* @__PURE__ */ jsx("span", {
1986
+ className: "um-liquid__body",
1987
+ children: bubbles.map((bubble, index) => /* @__PURE__ */ jsx("span", {
1988
+ className: "um-liquid__bubble um-deco",
1989
+ style: {
1990
+ left: `${bubble.left}%`,
1991
+ width: bubble.size,
1992
+ height: bubble.size,
1993
+ animationDelay: `${bubble.delay}s`,
1994
+ animationDuration: `calc(var(--um-time) * ${bubble.duration}s)`
1995
+ }
1996
+ }, index))
1997
+ })
1998
+ ]
1999
+ }), props.zoneContent ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
2000
+ className: "um-liquid__layer",
2001
+ children: content
2002
+ }), /* @__PURE__ */ jsx("div", {
2003
+ className: "um-liquid__layer um-liquid__layer--inverse",
2004
+ "aria-hidden": "true",
2005
+ children: content
2006
+ })] })]
2007
+ }),
2008
+ /* @__PURE__ */ jsxs("div", {
2009
+ className: "um-footer-row um-liquid__footer",
2010
+ children: [/* @__PURE__ */ jsx(UploadHint, {}), /* @__PURE__ */ jsxs("span", {
2011
+ className: "um-liquid__footer-actions",
2012
+ children: [/* @__PURE__ */ jsx(TransportBadge, {}), /* @__PURE__ */ jsx(BrowseButton, { variant: "soft" })]
2013
+ })]
2014
+ }),
2015
+ /* @__PURE__ */ jsx(PresetQueue, {
2016
+ props,
2017
+ progressVariant: "bar"
2018
+ })
2019
+ ]
2020
+ });
2021
+ }
2022
+ const liquidPreset = {
2023
+ ...PRESET_INFO["liquid"],
2024
+ Component: LiquidPreset
2025
+ };
2026
+ //#endregion
2027
+ //#region src/animations/magnetic.tsx
2028
+ function MagneticPreset(props) {
2029
+ const { dropzone, labels } = useUploaderContext();
2030
+ const { dragState, isWindowDrag, lastDrop } = dropzone;
2031
+ return /* @__PURE__ */ jsxs("div", {
2032
+ className: cx("um-magnetic", props.classNames?.root),
2033
+ "data-window-drag": isWindowDrag || void 0,
2034
+ children: [
2035
+ /* @__PURE__ */ jsxs("div", {
2036
+ ...dropzone.getRootProps({ trackPointer: true }),
2037
+ className: cx("um-zone", "um-magnetic__zone", props.classNames?.zone),
2038
+ children: [
2039
+ /* @__PURE__ */ jsx("span", {
2040
+ className: "um-magnetic__field um-deco",
2041
+ "aria-hidden": "true"
2042
+ }),
2043
+ /* @__PURE__ */ jsx("span", {
2044
+ className: "um-magnetic__glow",
2045
+ "aria-hidden": "true"
2046
+ }),
2047
+ props.zoneContent ?? /* @__PURE__ */ jsxs("div", {
2048
+ className: "um-magnetic__content",
2049
+ children: [
2050
+ /* @__PURE__ */ jsxs("span", {
2051
+ className: "um-magnetic__core",
2052
+ "data-snap": lastDrop ? "true" : void 0,
2053
+ children: [
2054
+ /* @__PURE__ */ jsx("span", {
2055
+ className: "um-magnetic__ring",
2056
+ "aria-hidden": "true"
2057
+ }),
2058
+ /* @__PURE__ */ jsx("span", {
2059
+ className: "um-magnetic__ring um-magnetic__ring--2",
2060
+ "aria-hidden": "true"
2061
+ }),
2062
+ /* @__PURE__ */ jsx(UploadIcon, { size: 24 })
2063
+ ]
2064
+ }, lastDrop?.at ?? 0),
2065
+ /* @__PURE__ */ jsx("strong", {
2066
+ className: "um-magnetic__title",
2067
+ children: dragState === "reject" ? labels.dropReject : dragState === "accept" ? labels.dropActive : props.title ?? "Pull files into the field"
2068
+ }),
2069
+ /* @__PURE__ */ jsxs("span", {
2070
+ className: "um-magnetic__sub",
2071
+ children: ["Drop anywhere here, paste, or ", /* @__PURE__ */ jsx(BrowseButton, {
2072
+ variant: "link",
2073
+ children: "browse"
2074
+ })]
2075
+ }),
2076
+ /* @__PURE__ */ jsx(UploadHint, {})
2077
+ ]
2078
+ })
2079
+ ]
2080
+ }),
2081
+ /* @__PURE__ */ jsx(PresetQueue, {
2082
+ props,
2083
+ progressVariant: "bar",
2084
+ allowPause: true,
2085
+ className: "um-magnetic__queue"
2086
+ }),
2087
+ /* @__PURE__ */ jsx(TransportBadge, { className: "um-magnetic__badge" })
2088
+ ]
2089
+ });
2090
+ }
2091
+ const magneticPreset = {
2092
+ ...PRESET_INFO["magnetic"],
2093
+ Component: MagneticPreset
2094
+ };
2095
+ //#endregion
2096
+ //#region src/animations/visualizers.tsx
2097
+ const shown = (item) => item.status === "success" ? 1 : item.progress;
2098
+ const vars = (item, extra = {}) => ({
2099
+ "--um-p": shown(item),
2100
+ "--k": KIND_COLORS[item.kind],
2101
+ ...extra
2102
+ });
2103
+ /** Image sharpens and saturates from the centre outward as bytes upload. */
2104
+ function BloomVisual({ item }) {
2105
+ const url = useFilePreview(item);
2106
+ const Glyph = KIND_ICONS[item.kind];
2107
+ return /* @__PURE__ */ jsx("span", {
2108
+ className: "um-bloom",
2109
+ "data-status": item.status,
2110
+ style: vars(item),
2111
+ children: url ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("img", {
2112
+ className: "um-bloom__under",
2113
+ src: url,
2114
+ alt: "",
2115
+ draggable: false
2116
+ }), /* @__PURE__ */ jsx("img", {
2117
+ className: "um-bloom__over",
2118
+ src: url,
2119
+ alt: "",
2120
+ draggable: false
2121
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2122
+ className: "um-bloom__under um-bloom__wash",
2123
+ children: /* @__PURE__ */ jsx(Glyph, { size: 28 })
2124
+ }), /* @__PURE__ */ jsxs("span", {
2125
+ className: "um-bloom__over um-bloom__wash um-bloom__wash--on",
2126
+ children: [/* @__PURE__ */ jsx(Glyph, { size: 28 }), /* @__PURE__ */ jsx("span", {
2127
+ className: "um-bloom__ext",
2128
+ children: getFileLabel(item)
2129
+ })]
2130
+ })] })
2131
+ });
2132
+ }
2133
+ /** A page whose lines are inked by a scanning beam. Text files show their real first lines. */
2134
+ function PageVisual({ item }) {
2135
+ const snippet = useTextSnippet(item, 10);
2136
+ const skeleton = useMemo(() => {
2137
+ const random = seededRandom(item.id);
2138
+ return Array.from({ length: 10 }, (_, index) => index === 0 ? 55 : index % 4 === 3 ? 40 + random() * 25 : 70 + random() * 28);
2139
+ }, [item.id]);
2140
+ const lines = snippet && snippet.length > 0 ? snippet : null;
2141
+ const count = lines ? lines.length : skeleton.length;
2142
+ return /* @__PURE__ */ jsxs("span", {
2143
+ className: "um-page",
2144
+ "data-status": item.status,
2145
+ "data-real": lines ? "true" : void 0,
2146
+ style: vars(item),
2147
+ children: [
2148
+ /* @__PURE__ */ jsx("span", {
2149
+ className: "um-page__lines",
2150
+ children: Array.from({ length: count }, (_, index) => /* @__PURE__ */ jsx("span", {
2151
+ className: "um-page__line",
2152
+ style: {
2153
+ "--li": (index + .5) / count,
2154
+ "--w": lines ? "auto" : `${skeleton[index]}%`
2155
+ },
2156
+ children: lines ? lines[index] : null
2157
+ }, index))
2158
+ }),
2159
+ /* @__PURE__ */ jsx("span", {
2160
+ className: "um-page__scan",
2161
+ "aria-hidden": "true"
2162
+ }),
2163
+ /* @__PURE__ */ jsx("span", {
2164
+ className: "um-page__fold",
2165
+ "aria-hidden": "true"
2166
+ }),
2167
+ /* @__PURE__ */ jsx("span", {
2168
+ className: "um-page__tag",
2169
+ children: /* @__PURE__ */ jsx(FileIcon, {
2170
+ kind: item.kind,
2171
+ label: getFileLabel(item),
2172
+ size: 22
2173
+ })
2174
+ }),
2175
+ item.status === "success" ? /* @__PURE__ */ jsx("span", {
2176
+ className: "um-page__stamp",
2177
+ "aria-hidden": "true",
2178
+ children: "Uploaded"
2179
+ }) : null
2180
+ ]
2181
+ });
2182
+ }
2183
+ const FRAMES = 6;
2184
+ /** Real frames extracted from the video light up one by one as it uploads. */
2185
+ function FilmstripVisual({ item }) {
2186
+ const frames = useVideoFrames(item.kind === "video" ? item.file : null, FRAMES);
2187
+ const still = useFilePreview(item.kind === "image" ? item : null);
2188
+ const lit = Math.floor(shown(item) * FRAMES + 1e-4);
2189
+ const Glyph = KIND_ICONS[item.kind];
2190
+ return /* @__PURE__ */ jsxs("span", {
2191
+ className: "um-film",
2192
+ "data-status": item.status,
2193
+ style: vars(item),
2194
+ children: [/* @__PURE__ */ jsx("span", {
2195
+ className: "um-film__strip",
2196
+ children: Array.from({ length: FRAMES }, (_, index) => {
2197
+ const src = frames[index] ?? still;
2198
+ return /* @__PURE__ */ jsx("span", {
2199
+ className: "um-film__frame",
2200
+ "data-lit": index < lit || void 0,
2201
+ style: { "--fi": index },
2202
+ children: src ? /* @__PURE__ */ jsx("img", {
2203
+ src,
2204
+ alt: "",
2205
+ draggable: false
2206
+ }) : /* @__PURE__ */ jsx(Glyph, { size: 16 })
2207
+ }, index);
2208
+ })
2209
+ }), /* @__PURE__ */ jsx("span", {
2210
+ className: "um-film__playhead",
2211
+ "aria-hidden": "true"
2212
+ })]
2213
+ });
2214
+ }
2215
+ const BARS = 48;
2216
+ /** Waveform of the actual audio (decoded locally); a stable pseudo-waveform for other files. */
2217
+ function WaveformVisual({ item }) {
2218
+ const { peaks, real } = useAudioPeaks(item.file, BARS, item.kind === "audio");
2219
+ const ripple = useBecameTrue(item.status === "success", 1400);
2220
+ const progress = shown(item);
2221
+ const head = Math.floor(progress * BARS);
2222
+ const layer = (variant) => /* @__PURE__ */ jsx("span", {
2223
+ className: `um-wave__layer um-wave__layer--${variant}`,
2224
+ "aria-hidden": "true",
2225
+ children: peaks.map((peak, index) => /* @__PURE__ */ jsx("span", {
2226
+ className: "um-wave__bar",
2227
+ "data-head": variant === "fill" && item.status === "uploading" && Math.abs(index - head) <= 2 ? "true" : void 0,
2228
+ style: {
2229
+ "--h": peak.toFixed(3),
2230
+ "--bi": index / BARS
2231
+ }
2232
+ }, index))
2233
+ });
2234
+ return /* @__PURE__ */ jsxs("span", {
2235
+ className: "um-wave",
2236
+ "data-status": item.status,
2237
+ "data-real": real || void 0,
2238
+ "data-ripple": ripple || void 0,
2239
+ style: vars(item),
2240
+ children: [layer("base"), layer("fill")]
2241
+ });
2242
+ }
2243
+ /** Play/pause a local preview of an audio file. The object URL is created on first play and revoked on unmount. */
2244
+ function AudioPreviewButton({ item }) {
2245
+ const { labels } = useUploaderContext();
2246
+ const audio = useRef(null);
2247
+ const url = useRef(null);
2248
+ const [playing, setPlaying] = useState(false);
2249
+ useEffect(() => () => {
2250
+ audio.current?.pause();
2251
+ if (url.current) URL.revokeObjectURL(url.current);
2252
+ }, []);
2253
+ if (item.kind !== "audio") return null;
2254
+ const toggle = () => {
2255
+ if (!audio.current) {
2256
+ url.current = URL.createObjectURL(item.file);
2257
+ audio.current = new Audio(url.current);
2258
+ audio.current.addEventListener("ended", () => setPlaying(false));
2259
+ audio.current.addEventListener("pause", () => setPlaying(false));
2260
+ audio.current.addEventListener("play", () => setPlaying(true));
2261
+ }
2262
+ if (audio.current.paused) audio.current.play().catch(() => setPlaying(false));
2263
+ else audio.current.pause();
2264
+ };
2265
+ return /* @__PURE__ */ jsx("button", {
2266
+ type: "button",
2267
+ className: "um-icon-btn um-wave__play",
2268
+ "aria-label": `${playing ? labels.pause : "Play"} preview of ${item.name}`,
2269
+ "aria-pressed": playing,
2270
+ onClick: (event) => {
2271
+ event.stopPropagation();
2272
+ toggle();
2273
+ },
2274
+ children: playing ? /* @__PURE__ */ jsx(PauseIcon, { size: 15 }) : /* @__PURE__ */ jsx(PlayIcon, { size: 15 })
2275
+ });
2276
+ }
2277
+ const GRID = 8;
2278
+ /** The file assembles from flying pixels in a shuffled order driven by progress (one CSS variable, no per-cell renders). */
2279
+ const PixelVisual = /* @__PURE__ */ memo(function PixelVisual({ item }) {
2280
+ const url = useFilePreview(item);
2281
+ const cells = useMemo(() => {
2282
+ const random = seededRandom(item.id);
2283
+ return Array.from({ length: 64 }, (_, i) => i).sort(() => random() - .5).map((rank, index) => ({
2284
+ index,
2285
+ rank: rank / 64,
2286
+ dx: (random() - .5) * 120,
2287
+ dy: (random() - .5) * 120,
2288
+ shade: 35 + random() * 60
2289
+ }));
2290
+ }, [item.id]);
2291
+ return /* @__PURE__ */ jsxs("span", {
2292
+ className: "um-pixel",
2293
+ "data-status": item.status,
2294
+ style: vars(item),
2295
+ children: [cells.map((cell) => {
2296
+ const row = Math.floor(cell.index / GRID);
2297
+ const col = cell.index % GRID;
2298
+ return /* @__PURE__ */ jsx("span", {
2299
+ className: "um-pixel__cell",
2300
+ style: {
2301
+ "--o": cell.rank.toFixed(3),
2302
+ "--dx": `${cell.dx.toFixed(0)}px`,
2303
+ "--dy": `${cell.dy.toFixed(0)}px`,
2304
+ background: url ? `url("${url}") ${col / 7 * 100}% ${row / 7 * 100}% / 800% 800%` : `color-mix(in oklab, var(--k) ${cell.shade.toFixed(0)}%, var(--um-surface))`
2305
+ }
2306
+ }, cell.index);
2307
+ }), !url ? /* @__PURE__ */ jsx("span", {
2308
+ className: "um-pixel__glyph",
2309
+ children: /* @__PURE__ */ jsx(FileIcon, {
2310
+ kind: item.kind,
2311
+ label: getFileLabel(item),
2312
+ size: 34
2313
+ })
2314
+ }) : null]
2315
+ });
2316
+ });
2317
+ const VISUALIZERS = {
2318
+ bloom: BloomVisual,
2319
+ page: PageVisual,
2320
+ filmstrip: FilmstripVisual,
2321
+ waveform: WaveformVisual,
2322
+ pixel: PixelVisual,
2323
+ card: ({ item }) => /* @__PURE__ */ jsx(FileIcon, {
2324
+ kind: item.kind,
2325
+ label: getFileLabel(item),
2326
+ size: 44
2327
+ })
2328
+ };
2329
+ const DEFAULT_KIND_VISUALIZERS = {
2330
+ image: "bloom",
2331
+ video: "filmstrip",
2332
+ audio: "waveform",
2333
+ pdf: "page",
2334
+ document: "page",
2335
+ text: "page",
2336
+ code: "page",
2337
+ data: "page",
2338
+ spreadsheet: "page",
2339
+ presentation: "pixel",
2340
+ archive: "pixel",
2341
+ unknown: "pixel"
2342
+ };
2343
+ /** Square tile with overlay meta – used by Bloom and Pixel presets. */
2344
+ const VisualTile = /* @__PURE__ */ memo(function VisualTile({ item, visual }) {
2345
+ const { labels } = useUploaderContext();
2346
+ const celebrate = useBecameTrue(item.status === "success", 1100);
2347
+ return /* @__PURE__ */ jsxs("div", {
2348
+ className: "um-tile",
2349
+ "data-status": item.status,
2350
+ "data-celebrate": celebrate || void 0,
2351
+ children: [
2352
+ /* @__PURE__ */ jsx("div", {
2353
+ className: "um-tile__visual",
2354
+ children: visual
2355
+ }),
2356
+ /* @__PURE__ */ jsxs("div", {
2357
+ className: "um-tile__overlay",
2358
+ children: [/* @__PURE__ */ jsx("span", {
2359
+ className: "um-tile__badge",
2360
+ "aria-hidden": "true",
2361
+ children: item.status === "success" ? /* @__PURE__ */ jsx(AnimatedCheck, { size: 18 }) : item.status === "uploading" ? `${Math.round(item.progress * 100)}%` : null
2362
+ }), /* @__PURE__ */ jsx(ItemActions, {
2363
+ item,
2364
+ className: "um-tile__actions"
2365
+ })]
2366
+ }),
2367
+ /* @__PURE__ */ jsxs("div", {
2368
+ className: "um-tile__meta",
2369
+ children: [/* @__PURE__ */ jsx("span", {
2370
+ className: "um-tile__name",
2371
+ title: item.name,
2372
+ children: item.name
2373
+ }), /* @__PURE__ */ jsx(ItemStatus, { item })]
2374
+ }),
2375
+ /* @__PURE__ */ jsx("span", {
2376
+ className: "um-sr-only",
2377
+ role: "progressbar",
2378
+ "aria-label": item.name,
2379
+ "aria-valuemin": 0,
2380
+ "aria-valuemax": 100,
2381
+ "aria-valuenow": Math.round(shown(item) * 100),
2382
+ "aria-valuetext": progressValueText(item, labels)
2383
+ }),
2384
+ celebrate ? /* @__PURE__ */ jsx(Burst, {
2385
+ seed: item.id,
2386
+ count: 10,
2387
+ spread: 60,
2388
+ shape: "petal"
2389
+ }) : null
2390
+ ]
2391
+ });
2392
+ });
2393
+ /** Row with a visual on the left – used by Filmstrip, Waveform and Adaptive presets. */
2394
+ const VisualRow = /* @__PURE__ */ memo(function VisualRow({ item, visual, visualKind, leading }) {
2395
+ const { labels } = useUploaderContext();
2396
+ const celebrate = useBecameTrue(item.status === "success", 1100);
2397
+ const failed = useBecameTrue(item.status === "error", 700);
2398
+ return /* @__PURE__ */ jsxs("div", {
2399
+ className: "um-vrow",
2400
+ "data-status": item.status,
2401
+ "data-visual": visualKind,
2402
+ "data-shake": failed || void 0,
2403
+ children: [
2404
+ leading,
2405
+ /* @__PURE__ */ jsx("div", {
2406
+ className: "um-vrow__visual",
2407
+ children: visual
2408
+ }),
2409
+ /* @__PURE__ */ jsxs("div", {
2410
+ className: "um-vrow__body",
2411
+ children: [
2412
+ /* @__PURE__ */ jsxs("div", {
2413
+ className: "um-card__row",
2414
+ children: [/* @__PURE__ */ jsx("span", {
2415
+ className: "um-card__name",
2416
+ title: item.name,
2417
+ children: item.name
2418
+ }), /* @__PURE__ */ jsx("span", {
2419
+ className: "um-card__size",
2420
+ children: formatBytes(item.size)
2421
+ })]
2422
+ }),
2423
+ /* @__PURE__ */ jsx(ItemStatus, { item }),
2424
+ /* @__PURE__ */ jsx(UploadProgress, {
2425
+ className: "um-sr-only",
2426
+ value: shown(item),
2427
+ status: item.status,
2428
+ label: item.name,
2429
+ valueText: progressValueText(item, labels)
2430
+ })
2431
+ ]
2432
+ }),
2433
+ /* @__PURE__ */ jsx(ItemActions, {
2434
+ item,
2435
+ allowPause: true
2436
+ }),
2437
+ celebrate ? /* @__PURE__ */ jsx(Burst, {
2438
+ seed: item.id,
2439
+ count: 10,
2440
+ spread: 50,
2441
+ className: "um-vrow__burst"
2442
+ }) : null
2443
+ ]
2444
+ });
2445
+ });
2446
+ function ZoneTile({ title, icon, className }) {
2447
+ const { dropzone, labels, hintId } = useUploaderContext();
2448
+ return /* @__PURE__ */ jsxs("div", {
2449
+ ...dropzone.getRootProps({
2450
+ focusable: true,
2451
+ "aria-label": labels.zoneLabel,
2452
+ "aria-describedby": hintId
2453
+ }),
2454
+ className: cx("um-zone", "um-zonetile", className),
2455
+ children: [/* @__PURE__ */ jsx("span", {
2456
+ className: "um-zonetile__icon",
2457
+ children: icon
2458
+ }), /* @__PURE__ */ jsx("span", {
2459
+ className: "um-zonetile__title",
2460
+ children: dropzone.dragState === "reject" ? labels.dropReject : dropzone.dragState === "accept" ? labels.dropActive : title
2461
+ })]
2462
+ });
2463
+ }
2464
+ //#endregion
2465
+ //#region src/animations/media.tsx
2466
+ /** Grid of tiles where the first tile is the drop zone. */
2467
+ function TileGrid({ props, variant, zoneTitle, zoneIcon, render }) {
2468
+ const { items } = useUploaderContext();
2469
+ const renderFile = useRenderFile(props);
2470
+ return /* @__PURE__ */ jsxs("div", {
2471
+ className: cx("um-media", `um-media--${variant}`, props.classNames?.root),
2472
+ children: [
2473
+ /* @__PURE__ */ jsxs("div", {
2474
+ className: "um-media__grid",
2475
+ children: [/* @__PURE__ */ jsx(ZoneTile, {
2476
+ title: props.title ? String(props.title) : zoneTitle,
2477
+ icon: zoneIcon,
2478
+ className: props.classNames?.zone
2479
+ }), /* @__PURE__ */ jsx(AnimatedList, {
2480
+ as: "div",
2481
+ items,
2482
+ className: cx("um-media__items", props.classNames?.list),
2483
+ itemClassName: props.classNames?.item,
2484
+ "aria-label": "Files",
2485
+ renderItem: (item) => renderFile(item, () => render(item))
2486
+ })]
2487
+ }),
2488
+ /* @__PURE__ */ jsxs("div", {
2489
+ className: "um-footer-row",
2490
+ children: [/* @__PURE__ */ jsx(UploadHint, {}), /* @__PURE__ */ jsxs("span", {
2491
+ className: "um-media__footer",
2492
+ children: [/* @__PURE__ */ jsx(TransportBadge, {}), /* @__PURE__ */ jsx(BrowseButton, { variant: "soft" })]
2493
+ })]
2494
+ }),
2495
+ props.showRejections !== false ? /* @__PURE__ */ jsx(UploadRejections, {}) : null
2496
+ ]
2497
+ });
2498
+ }
2499
+ /** Banner zone + list of rows. */
2500
+ function RowList({ props, variant, zoneTitle, zoneIcon, visualFor, leading }) {
2501
+ const { items, dropzone, labels } = useUploaderContext();
2502
+ const renderFile = useRenderFile(props);
2503
+ return /* @__PURE__ */ jsxs("div", {
2504
+ className: cx("um-media", `um-media--${variant}`, props.classNames?.root),
2505
+ children: [
2506
+ /* @__PURE__ */ jsxs("div", {
2507
+ ...dropzone.getRootProps(),
2508
+ className: cx("um-zone", "um-media__banner", props.classNames?.zone),
2509
+ children: [/* @__PURE__ */ jsx("span", {
2510
+ className: "um-media__deco um-deco",
2511
+ "aria-hidden": "true",
2512
+ children: Array.from({ length: 24 }, (_, index) => /* @__PURE__ */ jsx("span", { style: { "--i": index } }, index))
2513
+ }), props.zoneContent ?? /* @__PURE__ */ jsxs(Fragment, { children: [
2514
+ /* @__PURE__ */ jsx("span", {
2515
+ className: "um-media__banner-icon",
2516
+ children: zoneIcon
2517
+ }),
2518
+ /* @__PURE__ */ jsxs("span", {
2519
+ className: "um-media__banner-text",
2520
+ children: [/* @__PURE__ */ jsx("strong", { children: dropzone.dragState === "reject" ? labels.dropReject : dropzone.dragState === "accept" ? labels.dropActive : props.title ?? zoneTitle }), /* @__PURE__ */ jsxs("span", { children: [
2521
+ /* @__PURE__ */ jsx(BrowseButton, { variant: "link" }),
2522
+ " · ",
2523
+ /* @__PURE__ */ jsx(UploadHint, {})
2524
+ ] })]
2525
+ }),
2526
+ /* @__PURE__ */ jsx(TransportBadge, {})
2527
+ ] })]
2528
+ }),
2529
+ props.showRejections !== false ? /* @__PURE__ */ jsx(UploadRejections, {}) : null,
2530
+ /* @__PURE__ */ jsx(AnimatedList, {
2531
+ items,
2532
+ className: cx("um-media__rows", props.classNames?.list),
2533
+ itemClassName: props.classNames?.item,
2534
+ "aria-label": "Files",
2535
+ renderItem: (item) => renderFile(item, () => {
2536
+ const kind = visualFor(item);
2537
+ const Visual = VISUALIZERS[kind];
2538
+ return /* @__PURE__ */ jsx(VisualRow, {
2539
+ item,
2540
+ visual: /* @__PURE__ */ jsx(Visual, { item }),
2541
+ visualKind: kind,
2542
+ leading: leading?.(item)
2543
+ });
2544
+ })
2545
+ })
2546
+ ]
2547
+ });
2548
+ }
2549
+ const bloomPreset = {
2550
+ ...PRESET_INFO["bloom"],
2551
+ Component: (props) => /* @__PURE__ */ jsx(TileGrid, {
2552
+ props,
2553
+ variant: "bloom",
2554
+ zoneTitle: "Add photos",
2555
+ zoneIcon: /* @__PURE__ */ jsx(ImageIcon, { size: 26 }),
2556
+ render: (item) => /* @__PURE__ */ jsx(VisualTile, {
2557
+ item,
2558
+ visual: /* @__PURE__ */ jsx(BloomVisual, { item })
2559
+ })
2560
+ })
2561
+ };
2562
+ const pixelPreset = {
2563
+ ...PRESET_INFO["pixel"],
2564
+ Component: (props) => /* @__PURE__ */ jsx(TileGrid, {
2565
+ props,
2566
+ variant: "pixel",
2567
+ zoneTitle: "Drop to assemble",
2568
+ zoneIcon: /* @__PURE__ */ jsx(PlusIcon, { size: 26 }),
2569
+ render: (item) => /* @__PURE__ */ jsx(VisualTile, {
2570
+ item,
2571
+ visual: /* @__PURE__ */ jsx(PixelVisual, { item })
2572
+ })
2573
+ })
2574
+ };
2575
+ const pageRevealPreset = {
2576
+ ...PRESET_INFO["page-reveal"],
2577
+ Component: (props) => /* @__PURE__ */ jsx(TileGrid, {
2578
+ props,
2579
+ variant: "page",
2580
+ zoneTitle: "Add documents",
2581
+ zoneIcon: /* @__PURE__ */ jsx(UploadIcon, { size: 24 }),
2582
+ render: (item) => /* @__PURE__ */ jsx(VisualTile, {
2583
+ item,
2584
+ visual: /* @__PURE__ */ jsx(PageVisual, { item })
2585
+ })
2586
+ })
2587
+ };
2588
+ const filmstripPreset = {
2589
+ ...PRESET_INFO["filmstrip"],
2590
+ Component: (props) => /* @__PURE__ */ jsx(RowList, {
2591
+ props,
2592
+ variant: "film",
2593
+ zoneTitle: "Drop video clips",
2594
+ zoneIcon: /* @__PURE__ */ jsx(VideoIcon, { size: 24 }),
2595
+ visualFor: () => "filmstrip"
2596
+ })
2597
+ };
2598
+ const waveformPreset = {
2599
+ ...PRESET_INFO["waveform"],
2600
+ Component: (props) => /* @__PURE__ */ jsx(RowList, {
2601
+ props,
2602
+ variant: "wave",
2603
+ zoneTitle: "Drop audio files",
2604
+ zoneIcon: /* @__PURE__ */ jsx(MusicIcon, { size: 24 }),
2605
+ visualFor: () => "waveform",
2606
+ leading: (item) => /* @__PURE__ */ jsx(AudioPreviewButton, { item })
2607
+ })
2608
+ };
2609
+ function AdaptivePreset(props) {
2610
+ const mapping = {
2611
+ ...DEFAULT_KIND_VISUALIZERS,
2612
+ ...props.fileAnimations
2613
+ };
2614
+ return /* @__PURE__ */ jsx(RowList, {
2615
+ props,
2616
+ variant: "adaptive",
2617
+ zoneTitle: "Drop anything – each type gets its own motion",
2618
+ zoneIcon: /* @__PURE__ */ jsx(UploadIcon, { size: 24 }),
2619
+ visualFor: (item) => mapping[item.kind],
2620
+ leading: (item) => item.kind === "audio" ? /* @__PURE__ */ jsx(AudioPreviewButton, { item }) : null
2621
+ });
2622
+ }
2623
+ const adaptivePreset = {
2624
+ ...PRESET_INFO["adaptive"],
2625
+ Component: AdaptivePreset
2626
+ };
2627
+ //#endregion
2628
+ //#region src/animations/orbit.tsx
2629
+ const LANES = [
2630
+ 74,
2631
+ 104,
2632
+ 134
2633
+ ];
2634
+ const SPEEDS = [
2635
+ 9,
2636
+ 13,
2637
+ 18
2638
+ ];
2639
+ const MAX_PLANETS = 12;
2640
+ /** Stable lane/phase per file so planets never jump lanes when siblings land. */
2641
+ function laneOf(id, order) {
2642
+ let hash = 0;
2643
+ for (let i = 0; i < id.length; i += 1) hash = hash * 31 + id.charCodeAt(i) | 0;
2644
+ return {
2645
+ lane: order % LANES.length,
2646
+ phase: Math.abs(hash) % 100 / 100
2647
+ };
2648
+ }
2649
+ function OrbitPreset(props) {
2650
+ const { items, totals, dropzone, labels, motion } = useUploaderContext();
2651
+ const { title, detail } = useSummaryText();
2652
+ const planets = usePresence(items.filter((item) => item.status !== "success").slice(0, MAX_PLANETS), motion.reduced ? 0 : 900);
2653
+ const done = totals.isComplete && totals.error === 0;
2654
+ const order = new Map(items.map((item, index) => [item.id, index]));
2655
+ return /* @__PURE__ */ jsxs("div", {
2656
+ className: cx("um-orbit", props.classNames?.root),
2657
+ "data-active": totals.active > 0 || void 0,
2658
+ children: [/* @__PURE__ */ jsxs("div", {
2659
+ ...dropzone.getRootProps(),
2660
+ className: cx("um-zone", "um-orbit__zone", props.classNames?.zone),
2661
+ children: [/* @__PURE__ */ jsxs("div", {
2662
+ className: "um-orbit__system",
2663
+ "aria-hidden": "true",
2664
+ children: [
2665
+ LANES.map((radius) => /* @__PURE__ */ jsx("span", {
2666
+ className: "um-orbit__lane",
2667
+ style: { "--r": `${radius}px` }
2668
+ }, radius)),
2669
+ planets.map(({ item, exiting }) => {
2670
+ const { lane, phase } = laneOf(item.id, order.get(item.id) ?? 0);
2671
+ const base = LANES[lane];
2672
+ const pull = item.status === "uploading" ? item.progress : 0;
2673
+ const radius = exiting ? 0 : base * (1 - .55 * pull);
2674
+ return /* @__PURE__ */ jsx("span", {
2675
+ className: "um-orbit__track",
2676
+ "data-status": item.status,
2677
+ "data-exiting": exiting || void 0,
2678
+ style: {
2679
+ "--r": `${radius}px`,
2680
+ "--speed": `${SPEEDS[lane]}s`,
2681
+ "--delay": `${-phase * SPEEDS[lane]}s`,
2682
+ "--k": KIND_COLORS[item.kind]
2683
+ },
2684
+ children: /* @__PURE__ */ jsx("span", {
2685
+ className: "um-orbit__planet",
2686
+ children: /* @__PURE__ */ jsx("span", {
2687
+ className: "um-orbit__body",
2688
+ children: /* @__PURE__ */ jsx(FilePreview, {
2689
+ item,
2690
+ size: 30
2691
+ })
2692
+ })
2693
+ })
2694
+ }, item.id);
2695
+ }),
2696
+ /* @__PURE__ */ jsx("span", {
2697
+ className: "um-orbit__core",
2698
+ "data-pulse": totals.success > 0 || void 0,
2699
+ children: done ? /* @__PURE__ */ jsx(AnimatedCheck, { size: 40 }) : items.length > 0 ? /* @__PURE__ */ jsxs("span", {
2700
+ className: "um-orbit__percent",
2701
+ children: [Math.round(totals.progress * 100), "%"]
2702
+ }) : /* @__PURE__ */ jsx(UploadIcon, { size: 28 })
2703
+ }, totals.success)
2704
+ ]
2705
+ }), props.zoneContent ?? /* @__PURE__ */ jsxs("div", {
2706
+ className: "um-orbit__text",
2707
+ children: [
2708
+ /* @__PURE__ */ jsx("strong", { children: dropzone.dragState === "reject" ? labels.dropReject : dropzone.dragState === "accept" ? labels.dropActive : items.length > 0 ? title : props.title ?? "Put your files in orbit" }),
2709
+ /* @__PURE__ */ jsx("span", { children: items.length > 0 && detail ? detail : /* @__PURE__ */ jsxs(Fragment, { children: ["Drop files or ", /* @__PURE__ */ jsx(BrowseButton, {
2710
+ variant: "link",
2711
+ children: "browse"
2712
+ })] }) }),
2713
+ /* @__PURE__ */ jsx(UploadHint, {}),
2714
+ /* @__PURE__ */ jsx(TransportBadge, {})
2715
+ ]
2716
+ })]
2717
+ }), /* @__PURE__ */ jsx(PresetQueue, {
2718
+ props,
2719
+ progressVariant: "bar"
2720
+ })]
2721
+ });
2722
+ }
2723
+ const orbitPreset = {
2724
+ ...PRESET_INFO["orbit"],
2725
+ Component: OrbitPreset
2726
+ };
2727
+ //#endregion
2728
+ //#region src/animations/portal.tsx
2729
+ function PortalPreset(props) {
2730
+ const { dropzone, labels, store, totals, motion, items } = useUploaderContext();
2731
+ const portalRef = useRef(null);
2732
+ const { getLastDrop } = dropzone;
2733
+ const [ghosts, setGhosts] = useState([]);
2734
+ const { title, detail } = useSummaryText();
2735
+ const complete = totals.isComplete && totals.error === 0;
2736
+ const flash = useBecameTrue(complete, 1200);
2737
+ const absorbing = totals.active > 0;
2738
+ useEffect(() => {
2739
+ if (motion.reduced) return;
2740
+ return store.on("added", (added) => {
2741
+ const portal = portalRef.current?.getBoundingClientRect();
2742
+ const drop = getLastDrop();
2743
+ const recent = drop && Date.now() - drop.at < 400;
2744
+ const baseAngle = portal && recent ? Math.atan2(drop.y - (portal.top + portal.height / 2), drop.x - (portal.left + portal.width / 2)) : -Math.PI / 2;
2745
+ const baseRadius = portal && recent ? Math.min(220, Math.hypot(drop.x - (portal.left + portal.width / 2), drop.y - (portal.top + portal.height / 2))) : 150;
2746
+ const next = added.slice(0, 8).map((item, index) => ({
2747
+ id: item.id,
2748
+ kind: item.kind,
2749
+ label: getFileLabel(item),
2750
+ angle: baseAngle * 180 / Math.PI + index * 34,
2751
+ radius: Math.max(110, baseRadius + index * 8),
2752
+ delay: index * 90
2753
+ }));
2754
+ setGhosts((current) => [...current, ...next]);
2755
+ const ids = new Set(next.map((ghost) => ghost.id));
2756
+ setTimeout(() => setGhosts((current) => current.filter((ghost) => !ids.has(ghost.id))), 1400 + next.length * 90);
2757
+ });
2758
+ }, [
2759
+ store,
2760
+ motion.reduced,
2761
+ getLastDrop
2762
+ ]);
2763
+ return /* @__PURE__ */ jsxs("div", {
2764
+ className: cx("um-portal", props.classNames?.root),
2765
+ "data-active": absorbing || void 0,
2766
+ "data-complete": complete || void 0,
2767
+ children: [/* @__PURE__ */ jsxs("div", {
2768
+ ...dropzone.getRootProps(),
2769
+ className: cx("um-zone", "um-portal__zone", props.classNames?.zone),
2770
+ style: { "--um-p": totals.progress },
2771
+ children: [/* @__PURE__ */ jsxs("span", {
2772
+ className: "um-portal__gate",
2773
+ ref: portalRef,
2774
+ "aria-hidden": "true",
2775
+ "data-flash": flash || void 0,
2776
+ children: [
2777
+ /* @__PURE__ */ jsx("span", { className: "um-portal__halo" }),
2778
+ /* @__PURE__ */ jsx("span", { className: "um-portal__ring" }),
2779
+ /* @__PURE__ */ jsx("span", { className: "um-portal__ring um-portal__ring--inner" }),
2780
+ /* @__PURE__ */ jsx("svg", {
2781
+ className: "um-portal__progress",
2782
+ viewBox: "0 0 100 100",
2783
+ children: /* @__PURE__ */ jsx("circle", {
2784
+ cx: "50",
2785
+ cy: "50",
2786
+ r: "47",
2787
+ pathLength: 1
2788
+ })
2789
+ }),
2790
+ /* @__PURE__ */ jsx("span", {
2791
+ className: "um-portal__core",
2792
+ children: complete ? /* @__PURE__ */ jsx(AnimatedCheck, { size: 34 }) : null
2793
+ }),
2794
+ ghosts.map((ghost) => /* @__PURE__ */ jsx("span", {
2795
+ className: "um-portal__ghost",
2796
+ style: {
2797
+ "--a": `${ghost.angle}deg`,
2798
+ "--r": `${ghost.radius}px`,
2799
+ animationDelay: `${ghost.delay}ms`
2800
+ },
2801
+ children: /* @__PURE__ */ jsx(FileIcon, {
2802
+ kind: ghost.kind,
2803
+ label: ghost.label,
2804
+ size: 30
2805
+ })
2806
+ }, ghost.id))
2807
+ ]
2808
+ }), props.zoneContent ?? /* @__PURE__ */ jsxs("div", {
2809
+ className: "um-portal__text",
2810
+ children: [
2811
+ /* @__PURE__ */ jsx("strong", { children: dropzone.dragState === "reject" ? labels.dropReject : dropzone.dragState === "accept" ? labels.dropActive : items.length > 0 ? title : props.title ?? "Open a portal for your files" }),
2812
+ /* @__PURE__ */ jsx("span", { children: items.length > 0 && detail ? detail : /* @__PURE__ */ jsxs(Fragment, { children: ["Drop files into the portal or ", /* @__PURE__ */ jsx(BrowseButton, {
2813
+ variant: "link",
2814
+ children: "browse"
2815
+ })] }) }),
2816
+ /* @__PURE__ */ jsx(UploadHint, {}),
2817
+ /* @__PURE__ */ jsx(TransportBadge, {})
2818
+ ]
2819
+ })]
2820
+ }), /* @__PURE__ */ jsx(PresetQueue, {
2821
+ props,
2822
+ progressVariant: "beam"
2823
+ })]
2824
+ });
2825
+ }
2826
+ const portalPreset = {
2827
+ ...PRESET_INFO["portal"],
2828
+ Component: PortalPreset
2829
+ };
2830
+ //#endregion
2831
+ //#region src/animations/segment-ring.tsx
2832
+ const SIZE = 184;
2833
+ const STROKE = 12;
2834
+ const RADIUS = 86;
2835
+ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
2836
+ function SegmentRingPreset(props) {
2837
+ const { items, totals, dropzone, labels } = useUploaderContext();
2838
+ const { title, detail } = useSummaryText();
2839
+ const ringItems = items.filter((item) => item.status !== "canceled");
2840
+ const done = totals.isComplete && totals.error === 0;
2841
+ const celebrate = useBecameTrue(done, 1300);
2842
+ const floor = ringItems.reduce((sum, item) => sum + item.size, 0) * .05;
2843
+ const weights = ringItems.map((item) => Math.max(item.size, floor, 1));
2844
+ const weightSum = weights.reduce((sum, weight) => sum + weight, 0);
2845
+ const gap = ringItems.length > 1 ? 4 : 0;
2846
+ let cursor = 0;
2847
+ const segments = ringItems.map((item, index) => {
2848
+ const sweep = weights[index] / weightSum * 360;
2849
+ const start = cursor;
2850
+ cursor += sweep;
2851
+ return {
2852
+ item,
2853
+ start,
2854
+ length: Math.max(0, (sweep - gap) / 360 * CIRCUMFERENCE),
2855
+ progress: item.status === "success" ? 1 : item.progress
2856
+ };
2857
+ });
2858
+ return /* @__PURE__ */ jsxs("div", {
2859
+ className: cx("um-ring", props.classNames?.root),
2860
+ "data-done": done || void 0,
2861
+ children: [/* @__PURE__ */ jsxs("div", {
2862
+ ...dropzone.getRootProps(),
2863
+ className: cx("um-zone", "um-ring__zone", props.classNames?.zone),
2864
+ children: [/* @__PURE__ */ jsxs("div", {
2865
+ className: "um-ring__dial",
2866
+ children: [
2867
+ /* @__PURE__ */ jsxs("svg", {
2868
+ width: SIZE,
2869
+ height: SIZE,
2870
+ viewBox: `0 0 ${SIZE} ${SIZE}`,
2871
+ "aria-hidden": "true",
2872
+ children: [/* @__PURE__ */ jsx("circle", {
2873
+ className: "um-ring__base",
2874
+ cx: SIZE / 2,
2875
+ cy: SIZE / 2,
2876
+ r: RADIUS,
2877
+ strokeWidth: STROKE
2878
+ }), segments.map(({ item, start, length, progress }) => /* @__PURE__ */ jsxs("g", {
2879
+ className: "um-ring__segment",
2880
+ "data-status": item.status,
2881
+ style: {
2882
+ rotate: `${start - 90}deg`,
2883
+ "--k": KIND_COLORS[item.kind]
2884
+ },
2885
+ children: [/* @__PURE__ */ jsx("circle", {
2886
+ className: "um-ring__track",
2887
+ cx: SIZE / 2,
2888
+ cy: SIZE / 2,
2889
+ r: RADIUS,
2890
+ strokeWidth: STROKE,
2891
+ strokeDasharray: `${length} ${CIRCUMFERENCE}`
2892
+ }), /* @__PURE__ */ jsx("circle", {
2893
+ className: "um-ring__fill",
2894
+ cx: SIZE / 2,
2895
+ cy: SIZE / 2,
2896
+ r: RADIUS,
2897
+ strokeWidth: STROKE,
2898
+ strokeDasharray: `${length * progress} ${CIRCUMFERENCE}`
2899
+ })]
2900
+ }, item.id))]
2901
+ }),
2902
+ /* @__PURE__ */ jsx("div", {
2903
+ className: "um-ring__center",
2904
+ children: done ? /* @__PURE__ */ jsx(AnimatedCheck, { size: 52 }) : ringItems.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2905
+ className: "um-ring__percent",
2906
+ children: [Math.round(totals.progress * 100), "%"]
2907
+ }), /* @__PURE__ */ jsxs("span", {
2908
+ className: "um-ring__count",
2909
+ children: [
2910
+ totals.success,
2911
+ " of ",
2912
+ ringItems.length
2913
+ ]
2914
+ })] }) : /* @__PURE__ */ jsx(UploadIcon, { size: 30 })
2915
+ }),
2916
+ celebrate ? /* @__PURE__ */ jsx(Burst, {
2917
+ seed: "ring",
2918
+ count: 18,
2919
+ spread: 110
2920
+ }) : null
2921
+ ]
2922
+ }), props.zoneContent ?? /* @__PURE__ */ jsxs("div", {
2923
+ className: "um-ring__text",
2924
+ children: [
2925
+ /* @__PURE__ */ jsx("strong", { children: dropzone.dragState === "reject" ? labels.dropReject : dropzone.dragState === "accept" ? labels.dropActive : ringItems.length > 0 ? title : props.title ?? "Every file gets its own arc" }),
2926
+ /* @__PURE__ */ jsx("span", {
2927
+ className: "um-ring__detail",
2928
+ children: detail || (ringItems.length > 0 ? "Drop more files on the dial" : "Drop files on the dial")
2929
+ }),
2930
+ /* @__PURE__ */ jsx("span", {
2931
+ className: "um-ring__browse",
2932
+ children: /* @__PURE__ */ jsx(BrowseButton, { variant: "soft" })
2933
+ }),
2934
+ /* @__PURE__ */ jsx(UploadHint, {}),
2935
+ /* @__PURE__ */ jsx(TransportBadge, {})
2936
+ ]
2937
+ })]
2938
+ }), /* @__PURE__ */ jsx(PresetQueue, {
2939
+ props,
2940
+ progressVariant: "ring"
2941
+ })]
2942
+ });
2943
+ }
2944
+ const segmentRingPreset = {
2945
+ ...PRESET_INFO["segment-ring"],
2946
+ Component: SegmentRingPreset
2947
+ };
2948
+ //#endregion
2949
+ //#region src/animations/stack.tsx
2950
+ const VISIBLE = 5;
2951
+ function StackPreset(props) {
2952
+ const { items, dropzone, labels, motion, store } = useUploaderContext();
2953
+ const [showList, setShowList] = useState(false);
2954
+ const listId = useId();
2955
+ const renderFile = useRenderFile(props);
2956
+ const deck = items.filter((item) => item.status !== "success");
2957
+ const done = items.filter((item) => item.status === "success");
2958
+ const entries = usePresence(deck, motion.reduced ? 0 : 700);
2959
+ let depth = 0;
2960
+ return /* @__PURE__ */ jsxs("div", {
2961
+ className: cx("um-stack", props.classNames?.root),
2962
+ children: [
2963
+ /* @__PURE__ */ jsxs("div", {
2964
+ className: "um-stack__layout",
2965
+ children: [/* @__PURE__ */ jsxs("div", {
2966
+ ...dropzone.getRootProps({ noClick: deck.length > 0 }),
2967
+ className: cx("um-zone", "um-stack__deck", props.classNames?.zone),
2968
+ "data-count": Math.min(deck.length, VISIBLE),
2969
+ children: [
2970
+ deck.length === 0 && !entries.some((entry) => entry.exiting) ? /* @__PURE__ */ jsxs("div", {
2971
+ className: "um-stack__empty",
2972
+ children: [
2973
+ /* @__PURE__ */ jsx("span", {
2974
+ className: "um-stack__ghost",
2975
+ style: { "--d": 2 },
2976
+ "aria-hidden": "true"
2977
+ }),
2978
+ /* @__PURE__ */ jsx("span", {
2979
+ className: "um-stack__ghost",
2980
+ style: { "--d": 1 },
2981
+ "aria-hidden": "true"
2982
+ }),
2983
+ /* @__PURE__ */ jsx("span", {
2984
+ className: "um-stack__placeholder",
2985
+ children: props.zoneContent ?? /* @__PURE__ */ jsxs(Fragment, { children: [
2986
+ /* @__PURE__ */ jsx(PlusIcon, { size: 26 }),
2987
+ /* @__PURE__ */ jsx("strong", { children: dropzone.dragState === "reject" ? labels.dropReject : dropzone.dragState === "accept" ? labels.dropActive : props.title ?? "Stack files here" }),
2988
+ /* @__PURE__ */ jsx(BrowseButton, { variant: "link" }),
2989
+ /* @__PURE__ */ jsx(UploadHint, {})
2990
+ ] })
2991
+ })
2992
+ ]
2993
+ }) : null,
2994
+ entries.map(({ item, exiting }) => {
2995
+ const d = exiting ? -1 : depth++;
2996
+ if (d >= VISIBLE) return null;
2997
+ const landed = exiting && store.getItem(item.id)?.status === "success";
2998
+ return /* @__PURE__ */ jsx("div", {
2999
+ className: "um-stack__card",
3000
+ "data-status": item.status,
3001
+ "data-exit": exiting ? landed ? "land" : "discard" : void 0,
3002
+ "data-front": d === 0 || void 0,
3003
+ style: { "--d": Math.max(d, 0) },
3004
+ "aria-hidden": d !== 0 || exiting || void 0,
3005
+ inert: d !== 0 || exiting || void 0,
3006
+ children: renderFile(item, () => /* @__PURE__ */ jsxs(Fragment, { children: [
3007
+ /* @__PURE__ */ jsx(FilePreview, {
3008
+ item,
3009
+ size: 58
3010
+ }),
3011
+ /* @__PURE__ */ jsxs("span", {
3012
+ className: "um-stack__meta",
3013
+ children: [/* @__PURE__ */ jsx("span", {
3014
+ className: "um-stack__name",
3015
+ title: item.name,
3016
+ children: item.name
3017
+ }), /* @__PURE__ */ jsx("span", {
3018
+ className: "um-stack__size",
3019
+ children: formatBytes(item.size)
3020
+ })]
3021
+ }),
3022
+ /* @__PURE__ */ jsx(UploadProgress, {
3023
+ value: item.progress,
3024
+ status: item.status,
3025
+ label: item.name,
3026
+ valueText: progressValueText(item, labels)
3027
+ }),
3028
+ /* @__PURE__ */ jsxs("span", {
3029
+ className: "um-stack__foot",
3030
+ children: [/* @__PURE__ */ jsx(ItemStatus, { item }), /* @__PURE__ */ jsx(ItemActions, {
3031
+ item,
3032
+ allowPause: true
3033
+ })]
3034
+ })
3035
+ ] }))
3036
+ }, item.id);
3037
+ }),
3038
+ deck.length > VISIBLE ? /* @__PURE__ */ jsxs("span", {
3039
+ className: "um-stack__more",
3040
+ children: ["+", deck.length - VISIBLE]
3041
+ }) : null,
3042
+ deck.length > 0 ? /* @__PURE__ */ jsx(BrowseButton, {
3043
+ variant: "icon",
3044
+ className: "um-stack__add",
3045
+ "aria-label": labels.uploadMore,
3046
+ children: /* @__PURE__ */ jsx(PlusIcon, { size: 18 })
3047
+ }) : null
3048
+ ]
3049
+ }), /* @__PURE__ */ jsxs("div", {
3050
+ className: "um-stack__tray",
3051
+ children: [
3052
+ /* @__PURE__ */ jsxs("span", {
3053
+ className: "um-stack__tray-title",
3054
+ children: [
3055
+ labels.status.success,
3056
+ " ",
3057
+ /* @__PURE__ */ jsx("strong", { children: done.length })
3058
+ ]
3059
+ }),
3060
+ done.length === 0 ? /* @__PURE__ */ jsx("span", {
3061
+ className: "um-stack__tray-empty",
3062
+ children: "Finished files land here"
3063
+ }) : null,
3064
+ /* @__PURE__ */ jsx(AnimatedList, {
3065
+ items: done,
3066
+ className: "um-stack__done",
3067
+ "aria-label": labels.status.success,
3068
+ renderItem: (item) => /* @__PURE__ */ jsxs("span", {
3069
+ className: "um-stack__chip",
3070
+ title: item.name,
3071
+ children: [
3072
+ /* @__PURE__ */ jsx(FilePreview, {
3073
+ item,
3074
+ size: 28
3075
+ }),
3076
+ /* @__PURE__ */ jsx("span", {
3077
+ className: "um-stack__chip-name",
3078
+ children: item.name
3079
+ }),
3080
+ /* @__PURE__ */ jsx(AnimatedCheck, { size: 16 })
3081
+ ]
3082
+ })
3083
+ })
3084
+ ]
3085
+ })]
3086
+ }),
3087
+ /* @__PURE__ */ jsxs("div", {
3088
+ className: "um-footer-row",
3089
+ children: [/* @__PURE__ */ jsx(TransportBadge, {}), /* @__PURE__ */ jsx("button", {
3090
+ type: "button",
3091
+ className: "um-btn um-btn--link um-stack__toggle",
3092
+ "aria-expanded": showList,
3093
+ "aria-controls": listId,
3094
+ onClick: () => setShowList((value) => !value),
3095
+ disabled: items.length === 0,
3096
+ children: showList ? labels.collapse : `${labels.expand} (${items.length})`
3097
+ })]
3098
+ }),
3099
+ /* @__PURE__ */ jsx("div", {
3100
+ id: listId,
3101
+ hidden: !showList,
3102
+ children: /* @__PURE__ */ jsx(PresetQueue, {
3103
+ props,
3104
+ progressVariant: "bar",
3105
+ allowPause: true
3106
+ })
3107
+ })
3108
+ ]
3109
+ });
3110
+ }
3111
+ const stackPreset = {
3112
+ ...PRESET_INFO["stack"],
3113
+ Component: StackPreset
3114
+ };
3115
+ //#endregion
3116
+ //#region src/components/Menu.tsx
3117
+ /** Small accessible menu button: arrow-key navigation, Escape to close, focus restored to the trigger. */
3118
+ function ActionMenu({ label, icon, actions, className, align = "end" }) {
3119
+ const [open, setOpen] = useState(false);
3120
+ const triggerRef = useRef(null);
3121
+ const menuRef = useRef(null);
3122
+ const menuId = useId();
3123
+ useEffect(() => {
3124
+ if (!open) return;
3125
+ (menuRef.current?.querySelector("[role=\"menuitem\"]:not([disabled])"))?.focus();
3126
+ const onPointer = (event) => {
3127
+ const target = event.target;
3128
+ if (!menuRef.current?.contains(target) && !triggerRef.current?.contains(target)) setOpen(false);
3129
+ };
3130
+ document.addEventListener("pointerdown", onPointer);
3131
+ return () => document.removeEventListener("pointerdown", onPointer);
3132
+ }, [open]);
3133
+ const close = (restoreFocus) => {
3134
+ setOpen(false);
3135
+ if (restoreFocus) triggerRef.current?.focus();
3136
+ };
3137
+ const onMenuKeyDown = (event) => {
3138
+ const items = Array.from(menuRef.current?.querySelectorAll("[role=\"menuitem\"]:not([disabled])") ?? []);
3139
+ const index = items.indexOf(document.activeElement);
3140
+ if (event.key === "Escape") {
3141
+ event.preventDefault();
3142
+ event.stopPropagation();
3143
+ close(true);
3144
+ } else if (event.key === "ArrowDown") {
3145
+ event.preventDefault();
3146
+ items[(index + 1) % items.length]?.focus();
3147
+ } else if (event.key === "ArrowUp") {
3148
+ event.preventDefault();
3149
+ items[(index - 1 + items.length) % items.length]?.focus();
3150
+ } else if (event.key === "Home") {
3151
+ event.preventDefault();
3152
+ items[0]?.focus();
3153
+ } else if (event.key === "End") {
3154
+ event.preventDefault();
3155
+ items[items.length - 1]?.focus();
3156
+ } else if (event.key === "Tab") close(false);
3157
+ };
3158
+ return /* @__PURE__ */ jsxs("div", {
3159
+ className: cx("um-menu", className),
3160
+ "data-align": align,
3161
+ children: [/* @__PURE__ */ jsx("button", {
3162
+ ref: triggerRef,
3163
+ type: "button",
3164
+ className: "um-icon-btn um-menu__trigger",
3165
+ "aria-label": label,
3166
+ title: label,
3167
+ "aria-haspopup": "menu",
3168
+ "aria-expanded": open,
3169
+ "aria-controls": open ? menuId : void 0,
3170
+ onClick: (event) => {
3171
+ event.stopPropagation();
3172
+ setOpen((value) => !value);
3173
+ },
3174
+ children: icon
3175
+ }), open ? /* @__PURE__ */ jsx("div", {
3176
+ ref: menuRef,
3177
+ id: menuId,
3178
+ role: "menu",
3179
+ "aria-label": label,
3180
+ className: "um-menu__list",
3181
+ onKeyDown: onMenuKeyDown,
3182
+ onClick: (event) => event.stopPropagation(),
3183
+ children: actions.map((action) => /* @__PURE__ */ jsx("button", {
3184
+ type: "button",
3185
+ role: "menuitem",
3186
+ className: "um-menu__item",
3187
+ "data-tone": action.tone,
3188
+ disabled: action.disabled,
3189
+ onClick: () => {
3190
+ close(true);
3191
+ action.onSelect();
3192
+ },
3193
+ children: action.label
3194
+ }, action.id))
3195
+ }) : null]
3196
+ });
3197
+ }
3198
+ //#endregion
3199
+ //#region src/animations/wave-bar.tsx
3200
+ const BAR_WAVE = /* @__PURE__ */ wavePath(1200, 38, 3.2, 8);
3201
+ function WaveBarPreset(props) {
3202
+ const context = useUploaderContext();
3203
+ const { totals, items, dropzone, labels } = context;
3204
+ const [expanded, setExpanded] = useState(false);
3205
+ const drawerId = useId();
3206
+ const { title, detail } = useSummaryText();
3207
+ const state = items.length === 0 ? "idle" : totals.active > 0 ? "active" : totals.paused > 0 ? "paused" : totals.error > 0 ? "error" : totals.isComplete ? "done" : "idle";
3208
+ const celebrate = useBecameTrue(state === "done", 1400);
3209
+ const dragging = dropzone.dragState !== "idle";
3210
+ const progress = state === "done" ? 1 : totals.progress;
3211
+ const menuActions = [
3212
+ {
3213
+ id: "add",
3214
+ label: labels.uploadMore,
3215
+ onSelect: context.openFileDialog
3216
+ },
3217
+ {
3218
+ id: "retry",
3219
+ label: labels.retryFailed,
3220
+ onSelect: () => context.retry(),
3221
+ disabled: totals.error + totals.canceled === 0
3222
+ },
3223
+ {
3224
+ id: "clear-done",
3225
+ label: labels.clearCompleted,
3226
+ onSelect: () => context.clear("completed"),
3227
+ disabled: totals.success === 0
3228
+ },
3229
+ {
3230
+ id: "clear",
3231
+ label: labels.clearAll,
3232
+ onSelect: () => context.clear("all"),
3233
+ tone: "danger",
3234
+ disabled: items.length === 0
3235
+ }
3236
+ ];
3237
+ return /* @__PURE__ */ jsxs("div", {
3238
+ className: cx("um-wavebar", props.classNames?.root),
3239
+ "data-state": state,
3240
+ "data-drag": dropzone.dragState,
3241
+ "data-expanded": expanded || void 0,
3242
+ children: [
3243
+ /* @__PURE__ */ jsxs("div", {
3244
+ ...dropzone.getRootProps({ noClick: state !== "idle" }),
3245
+ className: cx("um-zone", "um-wavebar__bar", props.classNames?.zone),
3246
+ style: { "--um-p": progress },
3247
+ children: [
3248
+ /* @__PURE__ */ jsx("span", {
3249
+ className: "um-wavebar__fill",
3250
+ "aria-hidden": "true"
3251
+ }),
3252
+ /* @__PURE__ */ jsx("span", {
3253
+ className: "um-wavebar__edge",
3254
+ "aria-hidden": "true"
3255
+ }),
3256
+ /* @__PURE__ */ jsx("div", {
3257
+ className: "um-wavebar__text",
3258
+ children: state === "idle" ? props.zoneContent ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
3259
+ className: "um-wavebar__title",
3260
+ children: dropzone.dragState === "reject" ? labels.dropReject : dragging ? labels.dropActive : props.title ?? "Drop files to upload"
3261
+ }), /* @__PURE__ */ jsxs("span", {
3262
+ className: "um-wavebar__detail",
3263
+ children: [
3264
+ "or ",
3265
+ /* @__PURE__ */ jsx(BrowseButton, {
3266
+ variant: "link",
3267
+ children: "browse"
3268
+ }),
3269
+ " · ",
3270
+ /* @__PURE__ */ jsx(UploadHint, { className: "um-wavebar__hint" })
3271
+ ]
3272
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
3273
+ className: "um-wavebar__title",
3274
+ children: [
3275
+ state === "done" ? /* @__PURE__ */ jsx(AnimatedCheck, {
3276
+ size: 18,
3277
+ className: "um-wavebar__check"
3278
+ }) : null,
3279
+ title,
3280
+ /* @__PURE__ */ jsx(TransportBadge, { className: "um-wavebar__badge" })
3281
+ ]
3282
+ }), /* @__PURE__ */ jsx("span", {
3283
+ className: "um-wavebar__detail",
3284
+ "aria-hidden": state === "active" || void 0,
3285
+ children: detail || (state === "done" ? `${items.length} of ${items.length} complete` : labels.summary(totals))
3286
+ })] })
3287
+ }),
3288
+ /* @__PURE__ */ jsx("div", {
3289
+ className: "um-wavebar__controls",
3290
+ children: state === "idle" ? /* @__PURE__ */ jsx(BrowseButton, {
3291
+ variant: "icon",
3292
+ className: "um-wavebar__browse",
3293
+ "aria-label": labels.browse,
3294
+ children: /* @__PURE__ */ jsx(UploadIcon, { size: 18 })
3295
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3296
+ state === "active" ? /* @__PURE__ */ jsx(IconButton, {
3297
+ label: labels.pause,
3298
+ onClick: () => context.pause(),
3299
+ children: /* @__PURE__ */ jsx(PauseIcon, { size: 16 })
3300
+ }) : state === "paused" ? /* @__PURE__ */ jsx(IconButton, {
3301
+ label: labels.resume,
3302
+ onClick: () => context.resume(),
3303
+ tone: "accent",
3304
+ children: /* @__PURE__ */ jsx(PlayIcon, { size: 16 })
3305
+ }) : state === "error" ? /* @__PURE__ */ jsx(IconButton, {
3306
+ label: labels.retryFailed,
3307
+ onClick: () => context.retry(),
3308
+ tone: "accent",
3309
+ children: /* @__PURE__ */ jsx(RetryIcon, {
3310
+ size: 16,
3311
+ className: "um-retry-icon"
3312
+ })
3313
+ }) : null,
3314
+ state === "active" || state === "paused" ? /* @__PURE__ */ jsx(IconButton, {
3315
+ label: labels.cancelAll,
3316
+ onClick: () => context.cancel(),
3317
+ children: /* @__PURE__ */ jsx(CloseIcon, { size: 16 })
3318
+ }) : /* @__PURE__ */ jsx(IconButton, {
3319
+ label: labels.uploadMore,
3320
+ onClick: context.openFileDialog,
3321
+ tone: "accent",
3322
+ children: /* @__PURE__ */ jsx(UploadIcon, { size: 16 })
3323
+ }),
3324
+ /* @__PURE__ */ jsx("button", {
3325
+ type: "button",
3326
+ className: "um-icon-btn",
3327
+ "aria-expanded": expanded,
3328
+ "aria-controls": drawerId,
3329
+ "aria-label": expanded ? labels.collapse : labels.expand,
3330
+ title: expanded ? labels.collapse : labels.expand,
3331
+ onClick: (event) => {
3332
+ event.stopPropagation();
3333
+ setExpanded((value) => !value);
3334
+ },
3335
+ children: expanded ? /* @__PURE__ */ jsx(CollapseIcon, { size: 16 }) : /* @__PURE__ */ jsx(ExpandIcon, { size: 16 })
3336
+ }),
3337
+ /* @__PURE__ */ jsx(ActionMenu, {
3338
+ label: labels.moreActions,
3339
+ icon: /* @__PURE__ */ jsx(MoreIcon, { size: 16 }),
3340
+ actions: menuActions
3341
+ })
3342
+ ] })
3343
+ }),
3344
+ /* @__PURE__ */ jsx("span", {
3345
+ className: "um-wavebar__wave-clip",
3346
+ "aria-hidden": "true",
3347
+ children: /* @__PURE__ */ jsx("svg", {
3348
+ className: "um-wavebar__wave",
3349
+ viewBox: "0 0 1200 16",
3350
+ preserveAspectRatio: "none",
3351
+ children: /* @__PURE__ */ jsx("path", { d: BAR_WAVE })
3352
+ })
3353
+ }),
3354
+ state !== "idle" ? /* @__PURE__ */ jsx("span", {
3355
+ className: "um-sr-only",
3356
+ role: "progressbar",
3357
+ "aria-label": title,
3358
+ "aria-valuemin": 0,
3359
+ "aria-valuemax": 100,
3360
+ "aria-valuenow": Math.round(progress * 100),
3361
+ "aria-valuetext": detail || title
3362
+ }) : null,
3363
+ celebrate ? /* @__PURE__ */ jsx(Burst, {
3364
+ seed: "wavebar",
3365
+ count: 16,
3366
+ spread: 70,
3367
+ className: "um-wavebar__burst"
3368
+ }) : null
3369
+ ]
3370
+ }),
3371
+ /* @__PURE__ */ jsx("div", {
3372
+ className: "um-wavebar__drawer",
3373
+ id: drawerId,
3374
+ hidden: !expanded || items.length === 0,
3375
+ children: /* @__PURE__ */ jsx("div", {
3376
+ className: "um-wavebar__drawer-inner",
3377
+ children: /* @__PURE__ */ jsx(PresetQueue, {
3378
+ props,
3379
+ progressVariant: "wave",
3380
+ allowPause: true
3381
+ })
3382
+ })
3383
+ }),
3384
+ !expanded && props.showRejections !== false && context.rejections.length > 0 ? /* @__PURE__ */ jsx(UploadRejections, {}) : null
3385
+ ]
3386
+ });
3387
+ }
3388
+ const waveBarPreset = {
3389
+ ...PRESET_INFO["wave-bar"],
3390
+ Component: WaveBarPreset
3391
+ };
3392
+ //#endregion
3393
+ //#region src/animations/index.ts
3394
+ /** All built-in presets, in gallery order. */
3395
+ const presets = [
3396
+ cardFanPreset,
3397
+ waveBarPreset,
3398
+ hoopShotPreset,
3399
+ essentialPreset,
3400
+ segmentRingPreset,
3401
+ beamPreset,
3402
+ magneticPreset,
3403
+ portalPreset,
3404
+ liquidPreset,
3405
+ stackPreset,
3406
+ orbitPreset,
3407
+ bloomPreset,
3408
+ pageRevealPreset,
3409
+ filmstripPreset,
3410
+ waveformPreset,
3411
+ pixelPreset,
3412
+ adaptivePreset
3413
+ ];
3414
+ function getPreset(id) {
3415
+ return presets.find((preset) => preset.id === id);
3416
+ }
3417
+ //#endregion
3418
+ //#region src/hooks/useDropzone.ts
3419
+ const hasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files");
3420
+ function compose(...handlers) {
3421
+ return (event) => {
3422
+ for (const handler of handlers) handler?.(event);
3423
+ };
3424
+ }
3425
+ const INTERACTIVE = "button, a[href], input, select, textarea, [role=\"button\"], [role=\"menuitem\"], [data-um-no-open]";
3426
+ function useDropzone(options) {
3427
+ const { accept = [], multiple = true, maxFiles, disabled = false, capture, preventWindowDrop = true } = options;
3428
+ const inputRef = useRef(null);
3429
+ const onFilesRef = useRef(options.onFiles);
3430
+ onFilesRef.current = options.onFiles;
3431
+ const [dragState, setDragState] = useState("idle");
3432
+ const [isWindowDrag, setWindowDrag] = useState(false);
3433
+ const [lastDrop, setLastDrop] = useState(null);
3434
+ const lastDropRef = useRef(null);
3435
+ const getLastDrop = useCallback(() => lastDropRef.current, []);
3436
+ const depth = useRef(0);
3437
+ const pointerFrame = useRef(null);
3438
+ useEffect(() => {
3439
+ if (typeof window === "undefined" || disabled) return;
3440
+ let windowDepth = 0;
3441
+ const enter = (event) => {
3442
+ if (!hasFiles(event)) return;
3443
+ windowDepth += 1;
3444
+ setWindowDrag(true);
3445
+ };
3446
+ const leave = (event) => {
3447
+ if (!hasFiles(event)) return;
3448
+ windowDepth = Math.max(0, windowDepth - 1);
3449
+ if (windowDepth === 0) setWindowDrag(false);
3450
+ };
3451
+ const reset = () => {
3452
+ windowDepth = 0;
3453
+ setWindowDrag(false);
3454
+ };
3455
+ const guard = (event) => {
3456
+ if (!preventWindowDrop || event.defaultPrevented || !hasFiles(event)) return;
3457
+ event.preventDefault();
3458
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "none";
3459
+ };
3460
+ const drop = (event) => {
3461
+ guard(event);
3462
+ reset();
3463
+ };
3464
+ window.addEventListener("dragenter", enter);
3465
+ window.addEventListener("dragleave", leave);
3466
+ window.addEventListener("dragover", guard);
3467
+ window.addEventListener("drop", drop);
3468
+ window.addEventListener("dragend", reset);
3469
+ return () => {
3470
+ window.removeEventListener("dragenter", enter);
3471
+ window.removeEventListener("dragleave", leave);
3472
+ window.removeEventListener("dragover", guard);
3473
+ window.removeEventListener("drop", drop);
3474
+ window.removeEventListener("dragend", reset);
3475
+ };
3476
+ }, [disabled, preventWindowDrop]);
3477
+ useEffect(() => () => {
3478
+ if (pointerFrame.current !== null) cancelAnimationFrame(pointerFrame.current);
3479
+ }, []);
3480
+ const evaluate = useCallback((transfer) => {
3481
+ const files = Array.from(transfer?.items ?? []).filter((item) => item.kind === "file");
3482
+ if (!multiple && files.length > 1) return "reject";
3483
+ if (maxFiles !== void 0 && files.length > maxFiles) return "reject";
3484
+ if (accept.length > 0 && accept.every((token) => !token.startsWith(".")) && files.some((item) => item.type && !isFileAccepted({
3485
+ name: "",
3486
+ type: item.type
3487
+ }, accept))) return "reject";
3488
+ return "accept";
3489
+ }, [
3490
+ accept,
3491
+ multiple,
3492
+ maxFiles
3493
+ ]);
3494
+ const open = useCallback(() => {
3495
+ if (!disabled) inputRef.current?.click();
3496
+ }, [disabled]);
3497
+ const deliver = useCallback((list) => {
3498
+ const files = Array.from(list ?? []);
3499
+ if (files.length > 0) onFilesRef.current(files);
3500
+ }, []);
3501
+ const setPointer = (element, clientX, clientY) => {
3502
+ if (pointerFrame.current !== null) cancelAnimationFrame(pointerFrame.current);
3503
+ pointerFrame.current = requestAnimationFrame(() => {
3504
+ pointerFrame.current = null;
3505
+ const rect = element.getBoundingClientRect();
3506
+ if (rect.width === 0 || rect.height === 0) return;
3507
+ element.style.setProperty("--um-px", ((clientX - rect.left) / rect.width).toFixed(4));
3508
+ element.style.setProperty("--um-py", ((clientY - rect.top) / rect.height).toFixed(4));
3509
+ });
3510
+ };
3511
+ const resetPointer = (element) => {
3512
+ if (pointerFrame.current !== null) cancelAnimationFrame(pointerFrame.current);
3513
+ pointerFrame.current = null;
3514
+ element.style.setProperty("--um-px", "0.5");
3515
+ element.style.setProperty("--um-py", "0.5");
3516
+ };
3517
+ const getRootProps = (rootOptions = {}) => {
3518
+ const { focusable = false, trackPointer = false, noClick = false, ...rest } = rootOptions;
3519
+ const onDragEnter = (event) => {
3520
+ if (disabled || !hasFiles(event)) return;
3521
+ event.preventDefault();
3522
+ depth.current += 1;
3523
+ setDragState(evaluate(event.dataTransfer));
3524
+ };
3525
+ const onDragOver = (event) => {
3526
+ if (disabled || !hasFiles(event)) return;
3527
+ event.preventDefault();
3528
+ event.dataTransfer.dropEffect = "copy";
3529
+ if (trackPointer) setPointer(event.currentTarget, event.clientX, event.clientY);
3530
+ };
3531
+ const onDragLeave = (event) => {
3532
+ if (disabled || !hasFiles(event)) return;
3533
+ depth.current = Math.max(0, depth.current - 1);
3534
+ if (depth.current === 0) {
3535
+ setDragState("idle");
3536
+ if (trackPointer) resetPointer(event.currentTarget);
3537
+ }
3538
+ };
3539
+ const onDrop = (event) => {
3540
+ if (disabled || !hasFiles(event)) return;
3541
+ event.preventDefault();
3542
+ depth.current = 0;
3543
+ setDragState("idle");
3544
+ setWindowDrag(false);
3545
+ const point = {
3546
+ x: event.clientX,
3547
+ y: event.clientY,
3548
+ at: Date.now()
3549
+ };
3550
+ lastDropRef.current = point;
3551
+ setLastDrop(point);
3552
+ deliver(event.dataTransfer.files);
3553
+ };
3554
+ const onClick = (event) => {
3555
+ if (disabled || noClick) return;
3556
+ const interactive = event.target.closest(INTERACTIVE);
3557
+ if (interactive && interactive !== event.currentTarget && event.currentTarget.contains(interactive)) return;
3558
+ open();
3559
+ };
3560
+ const onKeyDown = (event) => {
3561
+ if (!focusable || event.target !== event.currentTarget) return;
3562
+ if (event.key === "Enter" || event.key === " ") {
3563
+ event.preventDefault();
3564
+ open();
3565
+ }
3566
+ };
3567
+ const onPaste = (event) => {
3568
+ if (disabled || event.clipboardData.files.length === 0) return;
3569
+ event.preventDefault();
3570
+ deliver(event.clipboardData.files);
3571
+ };
3572
+ const onPointerMove = trackPointer ? (event) => {
3573
+ if (event.pointerType === "mouse") setPointer(event.currentTarget, event.clientX, event.clientY);
3574
+ } : void 0;
3575
+ const onPointerLeave = trackPointer ? (event) => resetPointer(event.currentTarget) : void 0;
3576
+ return {
3577
+ ...rest,
3578
+ ...focusable ? {
3579
+ role: "button",
3580
+ tabIndex: disabled ? -1 : 0,
3581
+ "aria-disabled": disabled || void 0
3582
+ } : {},
3583
+ "data-um-drag": dragState,
3584
+ "data-um-window-drag": isWindowDrag ? "true" : void 0,
3585
+ "data-um-disabled": disabled ? "true" : void 0,
3586
+ onDragEnter: compose(rest.onDragEnter, onDragEnter),
3587
+ onDragOver: compose(rest.onDragOver, onDragOver),
3588
+ onDragLeave: compose(rest.onDragLeave, onDragLeave),
3589
+ onDrop: compose(rest.onDrop, onDrop),
3590
+ onClick: compose(rest.onClick, onClick),
3591
+ onKeyDown: compose(rest.onKeyDown, onKeyDown),
3592
+ onPaste: compose(rest.onPaste, onPaste),
3593
+ onPointerMove: compose(rest.onPointerMove, onPointerMove),
3594
+ onPointerLeave: compose(rest.onPointerLeave, onPointerLeave)
3595
+ };
3596
+ };
3597
+ const getInputProps = () => ({
3598
+ ref: inputRef,
3599
+ type: "file",
3600
+ multiple,
3601
+ accept: accept.length > 0 ? accept.join(",") : void 0,
3602
+ capture,
3603
+ disabled,
3604
+ tabIndex: -1,
3605
+ hidden: true,
3606
+ "aria-hidden": true,
3607
+ onChange: (event) => {
3608
+ deliver(event.currentTarget.files);
3609
+ event.currentTarget.value = "";
3610
+ }
3611
+ });
3612
+ return {
3613
+ dragState,
3614
+ isDragActive: dragState !== "idle",
3615
+ isWindowDrag,
3616
+ lastDrop,
3617
+ getLastDrop,
3618
+ open,
3619
+ inputRef,
3620
+ getRootProps,
3621
+ getInputProps
3622
+ };
3623
+ }
3624
+ //#endregion
3625
+ //#region src/hooks/useUploader.ts
3626
+ /** Subscribe to any upload store. */
3627
+ function useUploadSnapshot(store) {
3628
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
3629
+ }
3630
+ /**
3631
+ * Headless uploader: queue state, actions and drag-and-drop wiring with no UI.
3632
+ *
3633
+ * ```tsx
3634
+ * const uploader = useUploader({ upload: createXhrUploader({ url: '/api/upload' }) });
3635
+ * <div {...uploader.dropzone.getRootProps({ focusable: true })}>
3636
+ * <input {...uploader.dropzone.getInputProps()} />
3637
+ * </div>
3638
+ * ```
3639
+ */
3640
+ function useUploader(options = {}) {
3641
+ const { store: externalStore, disabled = false, capture, preventWindowDrop, ...uploaderOptions } = options;
3642
+ const [ownStore] = useState(() => externalStore ?? createUploadStore(uploaderOptions));
3643
+ const store = externalStore ?? ownStore;
3644
+ const owned = !externalStore;
3645
+ useIsomorphicLayoutEffect(() => {
3646
+ if (owned) store.setOptions(uploaderOptions);
3647
+ else {
3648
+ const defined = Object.fromEntries(Object.entries(uploaderOptions).filter(([, value]) => value !== void 0));
3649
+ store.setOptions({
3650
+ ...store.getInitialOptions(),
3651
+ ...defined
3652
+ });
3653
+ }
3654
+ });
3655
+ useEffect(() => {
3656
+ if (!owned) return;
3657
+ return () => store.dispose();
3658
+ }, [store, owned]);
3659
+ const snapshot = useUploadSnapshot(store);
3660
+ const acceptKey = Array.isArray(uploaderOptions.accept) ? uploaderOptions.accept.join(",") : uploaderOptions.accept;
3661
+ const accept = useMemo(() => parseAccept(acceptKey), [acceptKey]);
3662
+ const dropzone = useDropzone({
3663
+ onFiles: useCallback((files) => store.addFiles(files), [store]),
3664
+ accept,
3665
+ multiple: uploaderOptions.multiple !== false,
3666
+ maxFiles: uploaderOptions.multiple === false ? 1 : uploaderOptions.maxFiles,
3667
+ disabled,
3668
+ capture,
3669
+ preventWindowDrop
3670
+ });
3671
+ return {
3672
+ ...snapshot,
3673
+ store,
3674
+ dropzone,
3675
+ accept,
3676
+ disabled,
3677
+ addFiles: store.addFiles,
3678
+ openFileDialog: dropzone.open,
3679
+ start: store.start,
3680
+ cancel: store.cancel,
3681
+ retry: store.retry,
3682
+ pause: store.pause,
3683
+ resume: store.resume,
3684
+ remove: store.remove,
3685
+ clear: store.clear,
3686
+ dismissRejection: store.dismissRejection
3687
+ };
3688
+ }
3689
+ //#endregion
3690
+ //#region src/themes/index.ts
3691
+ const SHADOWS = {
3692
+ none: "none",
3693
+ soft: "0 1px 2px rgb(15 15 40 / 0.04), 0 8px 24px -8px rgb(15 15 40 / 0.12)",
3694
+ elevated: "0 2px 4px rgb(15 15 40 / 0.04), 0 24px 48px -16px rgb(15 15 40 / 0.22)"
3695
+ };
3696
+ const length = (value) => typeof value === "number" ? `${value}px` : value;
3697
+ /** Converts appearance + motion settings to inline CSS custom properties. Unset values fall back to the stylesheet. */
3698
+ function appearanceToStyle(appearance = {}, motion) {
3699
+ const vars = {
3700
+ "--um-accent": appearance.accent,
3701
+ "--um-accent-2": appearance.accentSecondary,
3702
+ "--um-bg": appearance.background,
3703
+ "--um-surface": appearance.surface,
3704
+ "--um-text": appearance.text,
3705
+ "--um-muted": appearance.mutedText,
3706
+ "--um-border": appearance.border,
3707
+ "--um-success": appearance.success,
3708
+ "--um-danger": appearance.danger,
3709
+ "--um-radius": length(appearance.radius),
3710
+ "--um-font": appearance.fontFamily,
3711
+ "--um-shadow": appearance.shadow ? SHADOWS[appearance.shadow] ?? appearance.shadow : void 0,
3712
+ "--um-zone-h": length(appearance.zoneHeight),
3713
+ "--um-time": motion.speed === 1 ? void 0 : (1 / motion.speed).toFixed(3),
3714
+ "--um-intensity": motion.intensity === 1 ? void 0 : motion.intensity,
3715
+ "--um-particles": motion.particles === 1 ? void 0 : motion.particles
3716
+ };
3717
+ const style = {};
3718
+ for (const [key, value] of Object.entries(vars)) if (value !== void 0 && value !== "") style[key] = value;
3719
+ return style;
3720
+ }
3721
+ function resolveMotion(motion = {}, prefersReduced) {
3722
+ const mode = motion.reducedMotion ?? "user";
3723
+ return {
3724
+ reduced: mode === "always" || mode === "user" && prefersReduced,
3725
+ speed: Math.min(4, Math.max(.25, motion.speed ?? 1)),
3726
+ intensity: Math.min(1.5, Math.max(0, motion.intensity ?? 1)),
3727
+ particles: Math.min(1, Math.max(0, motion.particles ?? 1))
3728
+ };
3729
+ }
3730
+ const defaultLabels = {
3731
+ dropTitle: "Drag & drop files here",
3732
+ dropHint: "or browse files on your computer",
3733
+ browse: "Browse files",
3734
+ dropActive: "Release to upload",
3735
+ dropReject: "Some of these files can’t be uploaded",
3736
+ zoneLabel: "Upload files",
3737
+ upload: "Upload",
3738
+ uploadMore: "Add more files",
3739
+ pause: "Pause",
3740
+ resume: "Resume",
3741
+ cancel: "Cancel",
3742
+ cancelAll: "Cancel all uploads",
3743
+ retry: "Retry",
3744
+ retryFailed: "Retry failed uploads",
3745
+ remove: "Remove",
3746
+ clearCompleted: "Clear completed",
3747
+ clearAll: "Clear all",
3748
+ expand: "Show files",
3749
+ collapse: "Hide files",
3750
+ moreActions: "More actions",
3751
+ dismiss: "Dismiss",
3752
+ finishing: "Finishing…",
3753
+ simulated: "Simulated",
3754
+ simulatedHint: "Demo transport: progress is simulated and no data leaves your browser.",
3755
+ noHandler: "Selected – no upload handler configured",
3756
+ status: {
3757
+ pending: "Ready to upload",
3758
+ queued: "Waiting…",
3759
+ uploading: "Uploading",
3760
+ paused: "Paused",
3761
+ success: "Uploaded",
3762
+ error: "Failed",
3763
+ canceled: "Canceled"
3764
+ },
3765
+ timeLeft: (duration) => `${duration} left`,
3766
+ retryingIn: (seconds) => `Retrying in ${seconds}s…`,
3767
+ summary: (t) => {
3768
+ const files = (n) => `${n} ${n === 1 ? "file" : "files"}`;
3769
+ if (t.count === 0) return "No files yet";
3770
+ if (t.uploading + t.queued > 0) return `Uploading ${files(t.uploading + t.queued)}`;
3771
+ if (t.paused > 0) return `${files(t.paused)} paused`;
3772
+ if (t.error > 0) return `${t.error} ${t.error === 1 ? "upload" : "uploads"} failed`;
3773
+ if (t.pending > 0) return `${files(t.pending)} ready`;
3774
+ if (t.success > 0) return `${files(t.success)} uploaded`;
3775
+ return files(t.count);
3776
+ },
3777
+ acceptHint: ({ types, maxSize, maxFiles }) => [
3778
+ types || "Any file type",
3779
+ maxSize && `up to ${maxSize}`,
3780
+ maxFiles && `max ${maxFiles} ${maxFiles === 1 ? "file" : "files"}`
3781
+ ].filter(Boolean).join(" · ")
3782
+ };
3783
+ //#endregion
3784
+ //#region src/components/Announcer.tsx
3785
+ const ZERO_WIDTH = String.fromCharCode(8203);
3786
+ /**
3787
+ * Visually hidden live regions announcing lifecycle changes (not every
3788
+ * progress tick). Errors use an assertive region.
3789
+ */
3790
+ function UploadAnnouncer() {
3791
+ const { store, labels } = useUploaderContext();
3792
+ const [polite, setPolite] = useState("");
3793
+ const [assertive, setAssertive] = useState("");
3794
+ useEffect(() => {
3795
+ let flip = false;
3796
+ const say = (setter, message) => {
3797
+ flip = !flip;
3798
+ setter(flip ? message : message + ZERO_WIDTH);
3799
+ };
3800
+ const offs = [
3801
+ store.on("added", (items) => say(setPolite, items.length === 1 ? `${items[0].name} added` : `${items.length} files added`)),
3802
+ store.on("rejected", (rejections) => say(setAssertive, rejections.map((rejection) => rejection.errors[0]?.message).join(" "))),
3803
+ store.on("start", (item) => say(setPolite, `${labels.status.uploading} ${item.name}`)),
3804
+ store.on("success", (item) => say(setPolite, `${item.name} ${labels.status.success.toLowerCase()}`)),
3805
+ store.on("error", (item) => say(setAssertive, `${item.name}: ${labels.status.error}. ${item.error?.message ?? ""}`)),
3806
+ store.on("cancel", (item) => say(setPolite, `${item.name} ${labels.status.canceled.toLowerCase()}`)),
3807
+ store.on("removed", (item) => say(setPolite, `${item.name} ${labels.remove.toLowerCase()}d`)),
3808
+ store.on("complete", (items) => {
3809
+ const ok = items.filter((item) => item.status === "success").length;
3810
+ if (items.length > 1) say(setPolite, `${ok} of ${items.length} uploads complete`);
3811
+ })
3812
+ ];
3813
+ return () => offs.forEach((off) => off());
3814
+ }, [store, labels]);
3815
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
3816
+ className: "um-sr-only",
3817
+ role: "status",
3818
+ "aria-live": "polite",
3819
+ "aria-atomic": "true",
3820
+ children: polite
3821
+ }), /* @__PURE__ */ jsx("div", {
3822
+ className: "um-sr-only",
3823
+ role: "alert",
3824
+ "aria-live": "assertive",
3825
+ "aria-atomic": "true",
3826
+ children: assertive
3827
+ })] });
3828
+ }
3829
+ //#endregion
3830
+ //#region src/components/Uploader.tsx
3831
+ /**
3832
+ * Root of the composable API. Owns the upload store, the hidden file input,
3833
+ * theme variables and the screen-reader announcer.
3834
+ */
3835
+ function Uploader(props) {
3836
+ const { appearance, motion, labels, className, style, id, children, ...options } = props;
3837
+ const api = useUploader(options);
3838
+ const prefersReduced = usePrefersReducedMotion();
3839
+ const resolvedMotion = useMemo(() => resolveMotion(motion, prefersReduced), [
3840
+ motion?.speed,
3841
+ motion?.intensity,
3842
+ motion?.particles,
3843
+ motion?.reducedMotion,
3844
+ prefersReduced
3845
+ ]);
3846
+ const mergedLabels = useMemo(() => ({
3847
+ ...defaultLabels,
3848
+ ...labels,
3849
+ status: {
3850
+ ...defaultLabels.status,
3851
+ ...labels?.status
3852
+ }
3853
+ }), [labels]);
3854
+ const hintId = useId();
3855
+ const maxFiles = options.multiple === false ? 1 : options.maxFiles ?? null;
3856
+ const hint = mergedLabels.acceptHint({
3857
+ types: describeAccept(api.accept),
3858
+ maxSize: options.maxSize ? formatBytes(options.maxSize) : "",
3859
+ maxFiles
3860
+ });
3861
+ const value = {
3862
+ ...api,
3863
+ labels: mergedLabels,
3864
+ motion: resolvedMotion,
3865
+ hint,
3866
+ hintId,
3867
+ autoUpload: options.autoUpload !== false
3868
+ };
3869
+ return /* @__PURE__ */ jsx(UploaderContext.Provider, {
3870
+ value,
3871
+ children: /* @__PURE__ */ jsxs("div", {
3872
+ id,
3873
+ className: cx("um-root", className),
3874
+ "data-um-theme": appearance?.theme ?? "light",
3875
+ "data-um-motion": resolvedMotion.reduced ? "reduced" : "full",
3876
+ "data-um-density": appearance?.density ?? "comfortable",
3877
+ "data-um-transport": api.transport,
3878
+ style: {
3879
+ ...appearanceToStyle(appearance, resolvedMotion),
3880
+ ...style
3881
+ },
3882
+ children: [
3883
+ /* @__PURE__ */ jsx("input", { ...api.dropzone.getInputProps() }),
3884
+ children,
3885
+ /* @__PURE__ */ jsx(UploadAnnouncer, {})
3886
+ ]
3887
+ })
3888
+ });
3889
+ }
3890
+ //#endregion
3891
+ //#region src/components/FileUpload.tsx
3892
+ /**
3893
+ * Ready-made animated uploader.
3894
+ *
3895
+ * ```tsx
3896
+ * <FileUpload
3897
+ * preset="wave-bar"
3898
+ * upload={createXhrUploader({ url: '/api/upload' })}
3899
+ * accept="image/*,.pdf"
3900
+ * maxSize={20_000_000}
3901
+ * />
3902
+ * ```
3903
+ */
3904
+ function FileUpload(props) {
3905
+ const { preset: presetInput = "essential", renderFile, zoneContent, classNames, progressVariant, layout, fileAnimations, showRejections, title, description, className, ...uploaderProps } = props;
3906
+ const preset = typeof presetInput === "string" ? getPreset(presetInput) : presetInput;
3907
+ if (!preset) throw new Error(`upload-motion: unknown preset "${String(presetInput)}".`);
3908
+ const { Component, defaults } = preset;
3909
+ return /* @__PURE__ */ jsx(Uploader, {
3910
+ ...uploaderProps,
3911
+ autoUpload: uploaderProps.autoUpload ?? defaults?.autoUpload ?? true,
3912
+ className: cx(`um-preset-${preset.id}`, className, classNames?.root),
3913
+ children: /* @__PURE__ */ jsx(Component, {
3914
+ renderFile,
3915
+ zoneContent,
3916
+ classNames,
3917
+ progressVariant: progressVariant ?? defaults?.progressVariant,
3918
+ layout: layout ?? defaults?.layout,
3919
+ fileAnimations,
3920
+ showRejections,
3921
+ title,
3922
+ description
3923
+ })
3924
+ });
3925
+ }
3926
+ //#endregion
3927
+ export { ActionMenu, AnimatedCheck, AnimatedList, AudioPreviewButton, BloomVisual, BrowseButton, Burst, DropzoneContent, FileCard, FileIcon, FilePreview, FileUpload, FilmstripVisual, IconButton, ItemActions, ItemStatus, PRESET_INFO, PageVisual, PixelVisual, StartUploadButton, TransportBadge, UploadAnnouncer, UploadDropzone, UploadError, UploadFileList, UploadHint, UploadProgress, UploadRejections, UploadSummary, Uploader, VisualRow, VisualTile, WaveformVisual, adaptivePreset, appearanceToStyle, beamPreset, bloomPreset, cardFanPreset, clamp01, computeTotals, createSimulatedUploader, createUploadStore, createXhrUploader, defaultLabels, describeAccept, describeItemStatus, essentialPreset, filmstripPreset, formatBytes, formatDuration, formatPercent, formatSpeed, getExtension, getFileKind, getFileLabel, getPreset, hoopShotPreset, httpErrorMessage, isFileAccepted, isPreviewableImage, isUploadError, liquidPreset, magneticPreset, orbitPreset, pageRevealPreset, parseAccept, pixelPreset, portalPreset, presetCatalog, presets, resolveMotion, segmentRingPreset, stackPreset, toUploadError, useAudioPeaks, useDropzone, useFilePreview, useFlip, usePrefersReducedMotion, usePresence, useSummaryText, useTextSnippet, useUploadSnapshot, useUploader, useUploaderContext, useVideoFrames, validateFile, waveBarPreset, wavePath, waveformPreset };