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