@uploadcn/react 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1224 @@
1
+ "use client";
2
+ import { UploadError as UploadError$1, createAdapter, createUploader, formatBytes, formatDuration, formatSpeed, getClipboardFiles, getDraggedTypes, getDroppedFiles, getUploadSummary, isActiveStatus, isFileDrag, isPreviewableImage, matchesAccept, toAcceptAttribute, validateFileCount, validateFiles } from "@uploadcn/core";
3
+ import * as React from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { compressImage, transformImage } from "@uploadcn/core/image";
6
+ //#region src/config.tsx
7
+ const UploadConfigContext = React.createContext({});
8
+ /**
9
+ * Configure uploads once — usually in your root layout — and every Upload
10
+ * component and block uses it. Nest providers to override per section.
11
+ *
12
+ * @example
13
+ * <UploadConfigProvider adapter={cloudinaryAdapter({ cloudName, uploadPreset })}>
14
+ * {children}
15
+ * </UploadConfigProvider>
16
+ */
17
+ function UploadConfigProvider({ children, ...config }) {
18
+ const value = {
19
+ ...React.useContext(UploadConfigContext),
20
+ ...stripUndefined(config)
21
+ };
22
+ return /* @__PURE__ */ jsx(UploadConfigContext.Provider, {
23
+ value,
24
+ children
25
+ });
26
+ }
27
+ /** The merged config from the nearest `UploadConfigProvider`s. */
28
+ function useUploadConfig() {
29
+ return React.useContext(UploadConfigContext);
30
+ }
31
+ function stripUndefined(value) {
32
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
33
+ }
34
+ //#endregion
35
+ //#region src/use-upload-selector.ts
36
+ /**
37
+ * Subscribes to a slice of uploader state. The component re-renders only
38
+ * when the selected value changes (by `isEqual`, default `Object.is`).
39
+ */
40
+ function useUploadSelector(uploader, selector, isEqual = Object.is) {
41
+ const cache = React.useRef(null);
42
+ const getSnapshot = () => {
43
+ const state = uploader.getState();
44
+ const previous = cache.current;
45
+ if (previous && previous.state === state && previous.selector === selector) return previous.selected;
46
+ const selected = selector(state);
47
+ const stable = previous && isEqual(previous.selected, selected) ? previous.selected : selected;
48
+ cache.current = {
49
+ state,
50
+ selector,
51
+ selected: stable
52
+ };
53
+ return stable;
54
+ };
55
+ return React.useSyncExternalStore(uploader.subscribe, getSnapshot, getSnapshot);
56
+ }
57
+ /** Shallow array equality, for selectors that return lists. */
58
+ function shallowArrayEqual(a, b) {
59
+ return a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
60
+ }
61
+ //#endregion
62
+ //#region src/context.ts
63
+ const UploadContext = React.createContext(null);
64
+ function useUploadContext() {
65
+ const context = React.useContext(UploadContext);
66
+ if (!context) throw new Error("Upload components must be rendered inside <Upload> (Upload.Root)");
67
+ return context;
68
+ }
69
+ const UploadItemContext = React.createContext(null);
70
+ /**
71
+ * The current item. Inside `<UploadItem>` it reads from context; pass an id
72
+ * to subscribe to any item from within `<Upload>`.
73
+ */
74
+ function useUploadItem(id) {
75
+ const fromContext = React.useContext(UploadItemContext);
76
+ const byId = useOptionalItem(React.useContext(UploadContext)?.uploader ?? null, id);
77
+ const item = id ? byId : fromContext;
78
+ if (!item) throw new Error(id ? `No upload item with id "${id}"` : "useUploadItem() must be used inside <UploadItem> or given an id");
79
+ return item;
80
+ }
81
+ const emptyState = {
82
+ items: [],
83
+ online: true
84
+ };
85
+ const noop = () => () => {};
86
+ const nullUploader = {
87
+ getState: () => emptyState,
88
+ subscribe: noop,
89
+ getItem: () => void 0
90
+ };
91
+ function useOptionalItem(uploader, id) {
92
+ const source = uploader ?? nullUploader;
93
+ return useUploadSelector(source, () => id ? source.getItem(id) : void 0);
94
+ }
95
+ //#endregion
96
+ //#region src/messages.ts
97
+ const defaultMessages = {
98
+ status: {
99
+ idle: "Ready",
100
+ validating: "Checking",
101
+ queued: "Queued",
102
+ uploading: "Uploading",
103
+ paused: "Paused",
104
+ processing: "Processing",
105
+ scanning: "Scanning",
106
+ success: "Uploaded",
107
+ error: "Failed",
108
+ cancelled: "Cancelled",
109
+ rejected: "Rejected"
110
+ },
111
+ retrying: "Retrying",
112
+ waitingForNetwork: "Waiting for network",
113
+ added: (count) => `${count} file${count === 1 ? "" : "s"} added`,
114
+ uploaded: (name) => `${name} uploaded`,
115
+ failed: (name, reason) => `${name} failed: ${reason}`,
116
+ rejected: (name, reason) => `${name} rejected: ${reason}`,
117
+ cancelled: (name) => `${name} cancelled`,
118
+ completed: (count) => `${count} upload${count === 1 ? "" : "s"} finished`,
119
+ remove: (name) => `Remove ${name}`,
120
+ cancel: (name) => `Cancel upload of ${name}`,
121
+ retry: (name) => `Retry upload of ${name}`,
122
+ pause: (name) => `Pause upload of ${name}`,
123
+ resume: (name) => `Resume upload of ${name}`,
124
+ progress: (name) => `Upload progress for ${name}`,
125
+ totalProgress: "Total upload progress"
126
+ };
127
+ /** A status label that also explains automatic retries and offline pauses. */
128
+ function getStatusLabel(item, messages = defaultMessages) {
129
+ if (item.status === "queued" && item.retryAt) return messages.retrying;
130
+ if (item.status === "paused" && item.pauseReason === "offline") return messages.waitingForNetwork;
131
+ return messages.status[item.status];
132
+ }
133
+ //#endregion
134
+ //#region src/render.ts
135
+ function mergeRefs(...refs) {
136
+ const live = refs.filter(Boolean);
137
+ if (live.length <= 1) return live[0];
138
+ return (value) => {
139
+ const cleanups = [];
140
+ for (const ref of live) if (typeof ref === "function") {
141
+ const cleanup = ref(value);
142
+ if (typeof cleanup === "function") cleanups.push(cleanup);
143
+ } else if (ref && typeof ref === "object" && "current" in ref) ref.current = value;
144
+ return () => {
145
+ for (const cleanup of cleanups) cleanup();
146
+ for (const ref of live) if (ref && typeof ref === "object" && "current" in ref) ref.current = null;
147
+ };
148
+ };
149
+ }
150
+ /**
151
+ * Merges two prop objects: event handlers run in sequence (unless the first
152
+ * calls `preventDefault`), classNames concatenate, styles shallow-merge,
153
+ * refs combine, and everything else from `overrides` wins.
154
+ */
155
+ function mergeProps(base, overrides) {
156
+ const merged = { ...base };
157
+ for (const [key, value] of Object.entries(overrides)) {
158
+ const current = merged[key];
159
+ if (/^on[A-Z]/.test(key) && typeof current === "function" && typeof value === "function") merged[key] = (...args) => {
160
+ value(...args);
161
+ if (!args[0]?.defaultPrevented) current(...args);
162
+ };
163
+ else if (key === "className" && current && value) merged[key] = `${current} ${value}`;
164
+ else if (key === "style" && current && value) merged[key] = {
165
+ ...current,
166
+ ...value
167
+ };
168
+ else if (key === "ref") merged[key] = mergeRefs(current, value);
169
+ else if (value !== void 0) merged[key] = value;
170
+ }
171
+ return merged;
172
+ }
173
+ /** Renders `tag`, or the user's `render` element/function with merged props. */
174
+ function renderElement(tag, props, render, state) {
175
+ if (!render) return React.createElement(tag, props);
176
+ if (typeof render === "function") return render(props, state);
177
+ return React.cloneElement(render, mergeProps(props, render.props));
178
+ }
179
+ /** Converts truthy booleans to `""` data attributes and drops falsy ones. */
180
+ function dataAttributes(attributes) {
181
+ const result = {};
182
+ for (const [key, value] of Object.entries(attributes)) if (value === true) result[`data-${key}`] = "";
183
+ else if (value !== false && value != null) result[`data-${key}`] = value;
184
+ return result;
185
+ }
186
+ //#endregion
187
+ //#region src/use-dropzone.ts
188
+ /**
189
+ * Drag-and-drop state and handlers for any element. Uses an enter/leave
190
+ * counter so moving over child elements doesn't flicker the drag state.
191
+ */
192
+ function useDropzone(options) {
193
+ const { onDrop, accept, multiple = true, disabled = false } = options;
194
+ const [state, setState] = React.useState({
195
+ isDragging: false,
196
+ isDragReject: false
197
+ });
198
+ const depth = React.useRef(0);
199
+ const evaluate = React.useCallback((dataTransfer) => {
200
+ const types = getDraggedTypes(dataTransfer);
201
+ const tooMany = !multiple && types.length > 1;
202
+ const badType = types.some((type) => type !== "" && !matchesAccept({
203
+ name: "",
204
+ type
205
+ }, accept));
206
+ return tooMany || badType;
207
+ }, [accept, multiple]);
208
+ const handlers = React.useMemo(() => ({
209
+ onDragEnter(event) {
210
+ if (disabled || !isFileDrag(event.dataTransfer)) return;
211
+ event.preventDefault();
212
+ depth.current++;
213
+ setState({
214
+ isDragging: true,
215
+ isDragReject: evaluate(event.dataTransfer)
216
+ });
217
+ },
218
+ onDragOver(event) {
219
+ if (disabled || !isFileDrag(event.dataTransfer)) return;
220
+ event.preventDefault();
221
+ event.dataTransfer.dropEffect = "copy";
222
+ },
223
+ onDragLeave(event) {
224
+ if (disabled || !isFileDrag(event.dataTransfer)) return;
225
+ depth.current = Math.max(0, depth.current - 1);
226
+ if (depth.current === 0) setState({
227
+ isDragging: false,
228
+ isDragReject: false
229
+ });
230
+ },
231
+ onDrop(event) {
232
+ if (disabled || !isFileDrag(event.dataTransfer)) return;
233
+ event.preventDefault();
234
+ depth.current = 0;
235
+ setState({
236
+ isDragging: false,
237
+ isDragReject: false
238
+ });
239
+ getDroppedFiles(event.dataTransfer).then((files) => {
240
+ if (files.length) onDrop(multiple ? files : files.slice(0, 1));
241
+ });
242
+ }
243
+ }), [
244
+ disabled,
245
+ evaluate,
246
+ multiple,
247
+ onDrop
248
+ ]);
249
+ return {
250
+ ...state,
251
+ handlers
252
+ };
253
+ }
254
+ //#endregion
255
+ //#region src/use-file-preview.ts
256
+ function getPreviewKind(file) {
257
+ if (isPreviewableImage(file)) return "image";
258
+ if (file.type.startsWith("video/")) return "video";
259
+ if (file.type.startsWith("audio/")) return "audio";
260
+ if (file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf")) return "pdf";
261
+ return "file";
262
+ }
263
+ async function createThumbnail(file, size) {
264
+ if (typeof createImageBitmap !== "function" || file.type === "image/svg+xml") return null;
265
+ const bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
266
+ try {
267
+ const scale = Math.min(1, size / Math.max(bitmap.width, bitmap.height));
268
+ if (scale === 1) return null;
269
+ const width = Math.round(bitmap.width * scale);
270
+ const height = Math.round(bitmap.height * scale);
271
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), {
272
+ width,
273
+ height
274
+ });
275
+ const context = canvas.getContext("2d");
276
+ if (!context) return null;
277
+ context.drawImage(bitmap, 0, 0, width, height);
278
+ if ("convertToBlob" in canvas) return canvas.convertToBlob({
279
+ type: "image/webp",
280
+ quality: .8
281
+ });
282
+ return new Promise((resolve) => canvas.toBlob(resolve, "image/webp", .8));
283
+ } finally {
284
+ bitmap.close();
285
+ }
286
+ }
287
+ /**
288
+ * An object URL for previewing a file, revoked automatically when the file
289
+ * changes or the component unmounts.
290
+ */
291
+ function useFilePreview(file, { thumbnailSize } = {}) {
292
+ const [preview, setPreview] = React.useState(null);
293
+ React.useEffect(() => {
294
+ if (!file) return;
295
+ let cancelled = false;
296
+ let objectUrl = null;
297
+ const source = file;
298
+ const publish = (blob) => {
299
+ if (cancelled) return;
300
+ objectUrl = URL.createObjectURL(blob);
301
+ setPreview({
302
+ source,
303
+ url: objectUrl
304
+ });
305
+ };
306
+ (thumbnailSize && file.type.startsWith("image/") ? createThumbnail(file, thumbnailSize).catch(() => null) : Promise.resolve(null)).then((blob) => publish(blob ?? source));
307
+ return () => {
308
+ cancelled = true;
309
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
310
+ };
311
+ }, [file, thumbnailSize]);
312
+ return preview && preview.source === file ? preview.url : null;
313
+ }
314
+ //#endregion
315
+ //#region src/use-uploader.ts
316
+ function splitOptions(options) {
317
+ const { onAdd, onReject, onSuccess, onError, onComplete, restore, ...engine } = options;
318
+ return {
319
+ callbacks: {
320
+ onAdd,
321
+ onReject,
322
+ onSuccess,
323
+ onError,
324
+ onComplete
325
+ },
326
+ restore,
327
+ engine
328
+ };
329
+ }
330
+ const NO_ADAPTER = "UploadCN: no adapter. Pass `adapter` to the component or useUploader, or wrap your app in <UploadConfigProvider adapter={…}>. See https://uploadcn.dev/docs/storage";
331
+ /**
332
+ * Stands in when no adapter is configured: the component still renders
333
+ * (and pre-renders), and every upload fails right away with the setup hint.
334
+ */
335
+ const missingAdapter = createAdapter(async () => {
336
+ throw new UploadError$1(NO_ADAPTER, {
337
+ code: "unknown",
338
+ retryable: false
339
+ });
340
+ }, { name: "missing" });
341
+ /**
342
+ * Creates an upload engine bound to the component's lifetime. The instance
343
+ * is stable; option changes (including a new adapter) apply on the next
344
+ * render without recreating it.
345
+ */
346
+ function useUploader(options = {}) {
347
+ const { messages: _messages, ...defaults } = useUploadConfig();
348
+ const { callbacks, restore, engine: own } = splitOptions(options);
349
+ const engine = {
350
+ ...defaults,
351
+ ...stripUndefined(own)
352
+ };
353
+ const configured = engine.adapter != null;
354
+ if (!configured) engine.adapter = missingAdapter;
355
+ const [uploader] = React.useState(() => createUploader(engine));
356
+ React.useEffect(() => {
357
+ if (!configured) console.error(NO_ADAPTER);
358
+ }, [configured]);
359
+ const callbacksRef = React.useRef(callbacks);
360
+ React.useEffect(() => {
361
+ callbacksRef.current = callbacks;
362
+ uploader.setOptions(engine);
363
+ });
364
+ React.useEffect(() => {
365
+ const unsubscribers = [
366
+ uploader.on("add", ({ items }) => callbacksRef.current.onAdd?.(items)),
367
+ uploader.on("reject", ({ item }) => callbacksRef.current.onReject?.(item)),
368
+ uploader.on("success", ({ item }) => callbacksRef.current.onSuccess?.(item)),
369
+ uploader.on("error", ({ item, error }) => callbacksRef.current.onError?.(item, error)),
370
+ uploader.on("complete", ({ items }) => callbacksRef.current.onComplete?.(items))
371
+ ];
372
+ const unmount = uploader.mount();
373
+ return () => {
374
+ for (const unsubscribe of unsubscribers) unsubscribe();
375
+ unmount();
376
+ };
377
+ }, [uploader]);
378
+ React.useEffect(() => {
379
+ if (restore) uploader.restore();
380
+ }, [uploader, restore]);
381
+ return uploader;
382
+ }
383
+ //#endregion
384
+ //#region src/primitives.tsx
385
+ const visuallyHidden = {
386
+ position: "absolute",
387
+ width: 1,
388
+ height: 1,
389
+ padding: 0,
390
+ margin: -1,
391
+ overflow: "hidden",
392
+ clip: "rect(0, 0, 0, 0)",
393
+ whiteSpace: "nowrap",
394
+ border: 0
395
+ };
396
+ const OPTION_KEYS = [
397
+ "adapter",
398
+ "autoUpload",
399
+ "concurrency",
400
+ "retry",
401
+ "duplicates",
402
+ "transform",
403
+ "process",
404
+ "persistence",
405
+ "network",
406
+ "progressInterval",
407
+ "accept",
408
+ "maxSize",
409
+ "minSize",
410
+ "maxFiles",
411
+ "minFiles",
412
+ "image",
413
+ "media",
414
+ "validate",
415
+ "restore",
416
+ "onAdd",
417
+ "onReject",
418
+ "onSuccess",
419
+ "onError",
420
+ "onComplete"
421
+ ];
422
+ const SUBMITTED = /* @__PURE__ */ new Set([
423
+ "idle",
424
+ "queued",
425
+ "uploading",
426
+ "paused",
427
+ "success"
428
+ ]);
429
+ /** A hidden file input that mirrors the queue, for native form submission. */
430
+ function FormFiles({ name, items }) {
431
+ const ref = React.useRef(null);
432
+ React.useEffect(() => {
433
+ const input = ref.current;
434
+ if (!input || typeof DataTransfer === "undefined") return;
435
+ const transfer = new DataTransfer();
436
+ for (const item of items) if (SUBMITTED.has(item.status)) transfer.items.add(item.file);
437
+ input.files = transfer.files;
438
+ }, [items]);
439
+ return /* @__PURE__ */ jsx("input", {
440
+ ref,
441
+ type: "file",
442
+ name,
443
+ multiple: true,
444
+ hidden: true,
445
+ tabIndex: -1,
446
+ "aria-hidden": true
447
+ });
448
+ }
449
+ function defaultFormValue(item) {
450
+ const result = item.result;
451
+ if (result && typeof result.key === "string") return result.key;
452
+ if (result && typeof result.url === "string") return result.url;
453
+ return item.name;
454
+ }
455
+ /**
456
+ * The root of an upload UI. Pass uploader options directly
457
+ * (`<Upload adapter={…} maxFiles={5}>`) or an `uploader` from `useUploader`
458
+ * to control it from outside.
459
+ */
460
+ function UploadRoot(props) {
461
+ if (props.uploader) return /* @__PURE__ */ jsx(UploadRootView, { ...props });
462
+ return /* @__PURE__ */ jsx(UploadRootWithUploader, { ...props });
463
+ }
464
+ function UploadRootWithUploader(props) {
465
+ const options = {};
466
+ const rest = {};
467
+ for (const [key, value] of Object.entries(props)) if (OPTION_KEYS.includes(key)) options[key] = value;
468
+ else rest[key] = value;
469
+ const uploader = useUploader(options);
470
+ return /* @__PURE__ */ jsx(UploadRootView, {
471
+ ...rest,
472
+ uploader
473
+ });
474
+ }
475
+ function UploadRootView({ uploader: typedUploader, multiple: multipleProp, disabled = false, name, getFormValue = defaultFormValue, submit = "value", capture, directory, messages: messagesProp, render, children, ...props }) {
476
+ const uploader = typedUploader;
477
+ const inputRef = React.useRef(null);
478
+ const inputId = React.useId();
479
+ const { accept, maxFiles } = uploader.getOptions();
480
+ const multiple = multipleProp ?? maxFiles !== 1;
481
+ const configMessages = useUploadConfig().messages;
482
+ const messages = React.useMemo(() => ({
483
+ ...defaultMessages,
484
+ ...configMessages,
485
+ ...messagesProp,
486
+ status: {
487
+ ...defaultMessages.status,
488
+ ...configMessages?.status,
489
+ ...messagesProp?.status
490
+ }
491
+ }), [configMessages, messagesProp]);
492
+ const items = useUploadSelector(uploader, (state) => state.items);
493
+ const isUploading = React.useMemo(() => getUploadSummary(items).isUploading, [items]);
494
+ const addFiles = React.useCallback((files) => {
495
+ if (disabled || files.length === 0) return;
496
+ if (!multiple) {
497
+ uploader.remove();
498
+ uploader.add(files.slice(0, 1));
499
+ return;
500
+ }
501
+ uploader.add(files);
502
+ }, [
503
+ disabled,
504
+ multiple,
505
+ uploader
506
+ ]);
507
+ const openFileDialog = React.useCallback(() => {
508
+ if (!disabled) inputRef.current?.click();
509
+ }, [disabled]);
510
+ const context = React.useMemo(() => ({
511
+ uploader,
512
+ openFileDialog,
513
+ addFiles,
514
+ accept,
515
+ multiple,
516
+ disabled,
517
+ messages,
518
+ inputId
519
+ }), [
520
+ uploader,
521
+ openFileDialog,
522
+ addFiles,
523
+ accept,
524
+ multiple,
525
+ disabled,
526
+ messages,
527
+ inputId
528
+ ]);
529
+ const state = {
530
+ disabled,
531
+ isUploading
532
+ };
533
+ const rootProps = mergeProps({
534
+ ...dataAttributes({
535
+ disabled,
536
+ uploading: isUploading
537
+ }),
538
+ "aria-busy": isUploading || void 0
539
+ }, props);
540
+ const directoryProps = directory ? {
541
+ webkitdirectory: "",
542
+ directory: ""
543
+ } : {};
544
+ return /* @__PURE__ */ jsx(UploadContext.Provider, {
545
+ value: context,
546
+ children: renderElement("div", {
547
+ ...rootProps,
548
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
549
+ children,
550
+ /* @__PURE__ */ jsx("input", {
551
+ ref: inputRef,
552
+ id: inputId,
553
+ type: "file",
554
+ tabIndex: -1,
555
+ "aria-hidden": true,
556
+ style: visuallyHidden,
557
+ accept: toAcceptAttribute(accept),
558
+ multiple,
559
+ disabled,
560
+ capture,
561
+ ...directoryProps,
562
+ onChange: (event) => {
563
+ addFiles(Array.from(event.currentTarget.files ?? []));
564
+ event.currentTarget.value = "";
565
+ }
566
+ }),
567
+ name && submit === "file" ? /* @__PURE__ */ jsx(FormFiles, {
568
+ name,
569
+ items
570
+ }) : null,
571
+ name && submit === "value" ? items.filter((item) => item.status === "success").map((item) => /* @__PURE__ */ jsx("input", {
572
+ type: "hidden",
573
+ name,
574
+ value: getFormValue(item)
575
+ }, item.id)) : null,
576
+ /* @__PURE__ */ jsx(UploadAnnouncer, {
577
+ uploader,
578
+ messages
579
+ })
580
+ ] })
581
+ }, render, state)
582
+ });
583
+ }
584
+ function UploadAnnouncer({ uploader, messages }) {
585
+ const [message, setMessage] = React.useState("");
586
+ const messagesRef = React.useRef(messages);
587
+ React.useEffect(() => {
588
+ messagesRef.current = messages;
589
+ });
590
+ React.useEffect(() => {
591
+ const m = () => messagesRef.current;
592
+ const unsubscribers = [
593
+ uploader.on("add", ({ items }) => setMessage(m().added(items.length))),
594
+ uploader.on("success", ({ item }) => setMessage(m().uploaded(item.name))),
595
+ uploader.on("error", ({ item, error }) => setMessage(m().failed(item.name, error.message))),
596
+ uploader.on("reject", ({ item }) => setMessage(m().rejected(item.name, item.issues[0]?.message ?? ""))),
597
+ uploader.on("cancel", ({ item }) => setMessage(m().cancelled(item.name)))
598
+ ];
599
+ return () => {
600
+ for (const unsubscribe of unsubscribers) unsubscribe();
601
+ };
602
+ }, [uploader]);
603
+ return /* @__PURE__ */ jsx("div", {
604
+ role: "status",
605
+ "aria-live": "polite",
606
+ "aria-atomic": true,
607
+ style: visuallyHidden,
608
+ children: message
609
+ });
610
+ }
611
+ const INTERACTIVE = "button, a, input, select, textarea, [role=button], [role=link]";
612
+ function UploadDropzone({ render, clickable = true, ...props }) {
613
+ const { addFiles, accept, multiple, disabled, openFileDialog } = useUploadContext();
614
+ const { isDragging, isDragReject, handlers } = useDropzone({
615
+ onDrop: addFiles,
616
+ accept,
617
+ multiple,
618
+ disabled
619
+ });
620
+ const state = {
621
+ isDragging,
622
+ isDragReject,
623
+ disabled
624
+ };
625
+ const interaction = clickable ? {
626
+ role: "button",
627
+ tabIndex: disabled ? -1 : 0,
628
+ "aria-disabled": disabled || void 0,
629
+ onClick(event) {
630
+ const target = event.target.closest(INTERACTIVE);
631
+ if (target && target !== event.currentTarget) return;
632
+ openFileDialog();
633
+ },
634
+ onKeyDown(event) {
635
+ if (event.target !== event.currentTarget) return;
636
+ if (event.key === "Enter" || event.key === " ") {
637
+ event.preventDefault();
638
+ openFileDialog();
639
+ }
640
+ }
641
+ } : {};
642
+ return renderElement("div", mergeProps({
643
+ ...handlers,
644
+ ...interaction,
645
+ ...dataAttributes({
646
+ dragging: isDragging,
647
+ "drag-reject": isDragReject,
648
+ disabled
649
+ })
650
+ }, props), render, state);
651
+ }
652
+ /** A button that opens the file picker. */
653
+ function UploadTrigger({ render, ...props }) {
654
+ const { openFileDialog, disabled, inputId } = useUploadContext();
655
+ return renderElement("button", mergeProps({
656
+ type: "button",
657
+ disabled,
658
+ "aria-controls": inputId,
659
+ onClick: openFileDialog
660
+ }, props), render, { disabled });
661
+ }
662
+ const ItemSlot = React.memo(function ItemSlot({ id, template }) {
663
+ const item = useUploadItem(id);
664
+ return /* @__PURE__ */ jsx(UploadItemContext.Provider, {
665
+ value: item,
666
+ children: typeof template === "function" ? template(item) : template
667
+ });
668
+ });
669
+ /**
670
+ * A list of uploads. Children are rendered once per item, with the item in
671
+ * context; each item re-renders on its own progress without re-rendering
672
+ * its siblings.
673
+ */
674
+ function UploadList({ render, children, filter, ...props }) {
675
+ const { uploader } = useUploadContext();
676
+ const ids = useUploadSelector(uploader, (state) => (filter ? state.items.filter(filter) : state.items).map((item) => item.id), shallowArrayEqual);
677
+ const content = ids.map((id) => /* @__PURE__ */ jsx(ItemSlot, {
678
+ id,
679
+ template: children
680
+ }, id));
681
+ return renderElement("ul", mergeProps({
682
+ role: "list",
683
+ ...dataAttributes({ empty: ids.length === 0 })
684
+ }, {
685
+ ...props,
686
+ children: content
687
+ }), render, { count: ids.length });
688
+ }
689
+ /** Provides an item to the item-level primitives inside it. */
690
+ function UploadItemRoot({ item: itemProp, render, ...props }) {
691
+ const contextItem = React.useContext(UploadItemContext);
692
+ const item = itemProp ?? contextItem;
693
+ if (!item) throw new Error("UploadItem needs an `item` prop or must be rendered inside UploadList");
694
+ return /* @__PURE__ */ jsx(UploadItemContext.Provider, {
695
+ value: item,
696
+ children: renderElement("li", mergeProps({
697
+ ...dataAttributes({
698
+ status: item.status,
699
+ restored: item.restored
700
+ }),
701
+ "aria-busy": isActiveStatus(item.status) || void 0
702
+ }, props), render, item)
703
+ });
704
+ }
705
+ /** An image thumbnail of the item, or `fallback` for other file types. */
706
+ function UploadPreview({ render, fallback = null, thumbnailSize = 256, alt = "", ...props }) {
707
+ const item = useUploadItem();
708
+ const isImage = getPreviewKind(item.file) === "image";
709
+ const url = useFilePreview(isImage ? item.originalFile : null, { thumbnailSize });
710
+ if (!isImage) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
711
+ if (!url) return null;
712
+ return renderElement("img", mergeProps({
713
+ src: url,
714
+ alt,
715
+ decoding: "async",
716
+ draggable: false
717
+ }, props), render, item);
718
+ }
719
+ function UploadName({ render, ...props }) {
720
+ const item = useUploadItem();
721
+ return renderElement("span", mergeProps({
722
+ title: item.name,
723
+ children: item.name
724
+ }, props), render, item);
725
+ }
726
+ function UploadSize({ render, format = "progress", ...props }) {
727
+ const item = useUploadItem();
728
+ return renderElement("span", mergeProps({ children: format === "progress" && (item.status === "uploading" || item.status === "paused") ? `${formatBytes(item.progress.loaded)} / ${formatBytes(item.size)}` : formatBytes(item.size) }, props), render, item);
729
+ }
730
+ /** Upload speed and time remaining, while uploading. */
731
+ function UploadSpeed({ render, ...props }) {
732
+ const item = useUploadItem();
733
+ if (item.status !== "uploading" || item.progress.speed == null) return null;
734
+ const eta = formatDuration(item.progress.eta);
735
+ return renderElement("span", mergeProps({ children: `${formatSpeed(item.progress.speed)}${eta ? ` · ${eta} left` : ""}` }, props), render, item);
736
+ }
737
+ function UploadStatus({ render, ...props }) {
738
+ const item = useUploadItem();
739
+ const { messages } = useUploadContext();
740
+ return renderElement("span", mergeProps({
741
+ ...dataAttributes({ status: item.status }),
742
+ children: getStatusLabel(item, messages)
743
+ }, props), render, item);
744
+ }
745
+ /** The item's error or validation message, when it has one. */
746
+ function UploadError({ render, ...props }) {
747
+ const item = useUploadItem();
748
+ if (!item.error) return null;
749
+ if (item.status !== "error" && item.status !== "rejected") return null;
750
+ const message = item.issues.length ? item.issues.map((issue) => issue.message).join(". ") : item.error.message;
751
+ return renderElement("p", mergeProps({
752
+ ...dataAttributes({ status: item.status }),
753
+ children: message
754
+ }, props), render, item);
755
+ }
756
+ /** Renders its children only once the item uploaded successfully. */
757
+ function UploadSuccess({ children }) {
758
+ return useUploadItem().status === "success" ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
759
+ }
760
+ const ProgressContext = React.createContext(null);
761
+ /**
762
+ * An accessible progress bar. Inside `<UploadItem>` it tracks that item;
763
+ * anywhere else it tracks the whole queue.
764
+ */
765
+ function UploadProgress(props) {
766
+ const item = React.useContext(UploadItemContext);
767
+ return item ? /* @__PURE__ */ jsx(ItemProgress, {
768
+ item,
769
+ ...props
770
+ }) : /* @__PURE__ */ jsx(QueueProgress, { ...props });
771
+ }
772
+ function ItemProgress({ item, ...props }) {
773
+ const { messages } = useUploadContext();
774
+ const done = item.status === "success" || item.status === "processing" || item.status === "scanning";
775
+ const indeterminate = item.status === "validating" || item.status === "processing" || item.status === "scanning" || item.status === "uploading" && item.size === 0;
776
+ return /* @__PURE__ */ jsx(ProgressView, {
777
+ ...props,
778
+ percent: done ? 100 : item.progress.percent,
779
+ indeterminate,
780
+ complete: item.status === "success",
781
+ label: messages.progress(item.name),
782
+ status: item.status
783
+ });
784
+ }
785
+ function QueueProgress(props) {
786
+ const { uploader, messages } = useUploadContext();
787
+ const items = useUploadSelector(uploader, (state) => state.items);
788
+ const summary = React.useMemo(() => getUploadSummary(items), [items]);
789
+ return /* @__PURE__ */ jsx(ProgressView, {
790
+ ...props,
791
+ percent: summary.percent,
792
+ indeterminate: false,
793
+ complete: summary.isComplete,
794
+ label: messages.totalProgress
795
+ });
796
+ }
797
+ function ProgressView({ render, percent, indeterminate, complete, label, status, ...props }) {
798
+ const state = {
799
+ percent,
800
+ indeterminate,
801
+ complete
802
+ };
803
+ return /* @__PURE__ */ jsx(ProgressContext.Provider, {
804
+ value: state,
805
+ children: renderElement("div", mergeProps({
806
+ role: "progressbar",
807
+ "aria-label": label,
808
+ "aria-valuemin": 0,
809
+ "aria-valuemax": 100,
810
+ "aria-valuenow": indeterminate ? void 0 : Math.round(percent),
811
+ "aria-valuetext": indeterminate ? void 0 : `${Math.round(percent)}%`,
812
+ style: { "--upload-progress": `${percent}%` },
813
+ ...dataAttributes({
814
+ indeterminate,
815
+ complete,
816
+ status
817
+ })
818
+ }, props), render, state)
819
+ });
820
+ }
821
+ /** The filled part of `UploadProgress`. */
822
+ function UploadProgressIndicator({ render, ...props }) {
823
+ const state = React.useContext(ProgressContext);
824
+ if (!state) throw new Error("UploadProgressIndicator must be inside UploadProgress");
825
+ return renderElement("div", mergeProps({
826
+ style: state.indeterminate ? void 0 : { transform: `translateX(-${100 - state.percent}%)` },
827
+ ...dataAttributes({
828
+ indeterminate: state.indeterminate,
829
+ complete: state.complete
830
+ })
831
+ }, props), render, state);
832
+ }
833
+ function createItemAction(name, isAvailable, perform, label) {
834
+ function Action({ render, forceMount = false, ...props }) {
835
+ const item = useUploadItem();
836
+ const { uploader, messages } = useUploadContext();
837
+ const available = isAvailable(item, uploader);
838
+ if (!available && !forceMount) return null;
839
+ return renderElement("button", mergeProps({
840
+ type: "button",
841
+ disabled: !available,
842
+ "aria-label": label(messages, item.name),
843
+ onClick: () => perform(item, uploader)
844
+ }, props), render, item);
845
+ }
846
+ Action.displayName = name;
847
+ return Action;
848
+ }
849
+ const UploadRemove = createItemAction("UploadRemove", () => true, (item, uploader) => uploader.remove(item.id), (m, name) => m.remove(name));
850
+ const UploadCancel = createItemAction("UploadCancel", (item) => isActiveStatus(item.status) || item.status === "paused", (item, uploader) => uploader.cancel(item.id), (m, name) => m.cancel(name));
851
+ const UploadRetry = createItemAction("UploadRetry", (item) => item.status === "error" || item.status === "cancelled", (item, uploader) => uploader.retry(item.id), (m, name) => m.retry(name));
852
+ const UploadPause = createItemAction("UploadPause", (item, uploader) => uploader.canPause(item), (item, uploader) => uploader.pause(item.id), (m, name) => m.pause(name));
853
+ const UploadResume = createItemAction("UploadResume", (item) => item.status === "paused", (item, uploader) => uploader.resume(item.id), (m, name) => m.resume(name));
854
+ /** Renders its children only while the queue is empty. */
855
+ function UploadEmpty({ children }) {
856
+ const { uploader } = useUploadContext();
857
+ return useUploadSelector(uploader, (state) => state.items.length === 0) ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
858
+ }
859
+ /** Starts idle uploads (when `autoUpload` is off). Hidden when there are none. */
860
+ function UploadStart({ render, forceMount = false, ...props }) {
861
+ const { uploader, disabled } = useUploadContext();
862
+ const count = useUploadSelector(uploader, (state) => state.items.filter((item) => item.status === "idle").length);
863
+ if (count === 0 && !forceMount) return null;
864
+ return renderElement("button", mergeProps({
865
+ type: "button",
866
+ disabled: disabled || count === 0,
867
+ onClick: () => uploader.start()
868
+ }, props), render, { count });
869
+ }
870
+ /** Removes finished items (or every item with `all`). Hidden when there's nothing to clear. */
871
+ function UploadClear({ render, forceMount = false, all = false, ...props }) {
872
+ const { uploader } = useUploadContext();
873
+ const count = useUploadSelector(uploader, (state) => all ? state.items.length : state.items.filter((item) => [
874
+ "success",
875
+ "cancelled",
876
+ "rejected"
877
+ ].includes(item.status)).length);
878
+ if (count === 0 && !forceMount) return null;
879
+ return renderElement("button", mergeProps({
880
+ type: "button",
881
+ disabled: count === 0,
882
+ onClick: () => all ? uploader.remove() : uploader.clearCompleted()
883
+ }, props), render, { count });
884
+ }
885
+ //#endregion
886
+ //#region src/use-upload.ts
887
+ /**
888
+ * The all-in-one hook: creates an uploader and returns its items, an
889
+ * aggregate summary and bound actions.
890
+ *
891
+ * @example
892
+ * const { items, add, summary } = useUpload({ adapter: s3Adapter({ endpoint: "/api/upload" }) })
893
+ */
894
+ function useUpload(options) {
895
+ return useUploadState(useUploader(options));
896
+ }
897
+ /** Like `useUpload`, for an uploader created elsewhere (e.g. from context). */
898
+ function useUploadState(uploader) {
899
+ const state = useUploadSelector(uploader, (s) => s);
900
+ const summary = React.useMemo(() => getUploadSummary(state.items), [state.items]);
901
+ return React.useMemo(() => ({
902
+ uploader,
903
+ items: state.items,
904
+ summary,
905
+ online: state.online,
906
+ add: uploader.add,
907
+ start: uploader.start,
908
+ pause: uploader.pause,
909
+ resume: uploader.resume,
910
+ cancel: uploader.cancel,
911
+ retry: uploader.retry,
912
+ remove: uploader.remove,
913
+ clearCompleted: uploader.clearCompleted,
914
+ update: uploader.update
915
+ }), [
916
+ uploader,
917
+ state,
918
+ summary
919
+ ]);
920
+ }
921
+ /** Aggregate progress, speed, ETA and counts for the whole queue. */
922
+ function useUploadProgress(uploader) {
923
+ const items = useUploadSelector(uploader, (state) => state.items);
924
+ return React.useMemo(() => getUploadSummary(items), [items]);
925
+ }
926
+ /**
927
+ * Derives form values from successful uploads — e.g. object keys for a
928
+ * server action or react-hook-form field. Pass a stable `getValue` (defined
929
+ * outside the component or memoized) to keep the returned array stable.
930
+ */
931
+ function useUploadValue(uploader, getValue) {
932
+ const items = useUploadSelector(uploader, (state) => state.items);
933
+ return React.useMemo(() => items.filter((item) => item.status === "success").map((item) => getValue(item)), [items, getValue]);
934
+ }
935
+ //#endregion
936
+ //#region src/use-window-drop.ts
937
+ /**
938
+ * Detects files dragged over the browser window — for "drop anywhere"
939
+ * overlays. Drops already handled by an inner dropzone (which calls
940
+ * `preventDefault`) are ignored, so the two can coexist.
941
+ */
942
+ function useWindowDrop({ onDrop, enabled = true }) {
943
+ const [isDragging, setIsDragging] = React.useState(false);
944
+ const onDropRef = React.useRef(onDrop);
945
+ React.useEffect(() => {
946
+ onDropRef.current = onDrop;
947
+ });
948
+ React.useEffect(() => {
949
+ if (!enabled) return;
950
+ let depth = 0;
951
+ function onDragEnter(event) {
952
+ if (!isFileDrag(event.dataTransfer)) return;
953
+ depth++;
954
+ setIsDragging(true);
955
+ }
956
+ function onDragOver(event) {
957
+ if (!isFileDrag(event.dataTransfer)) return;
958
+ event.preventDefault();
959
+ }
960
+ function onDragLeave(event) {
961
+ if (!isFileDrag(event.dataTransfer)) return;
962
+ depth = Math.max(0, depth - 1);
963
+ if (depth === 0 || event.relatedTarget === null) {
964
+ depth = 0;
965
+ setIsDragging(false);
966
+ }
967
+ }
968
+ function onDropEvent(event) {
969
+ depth = 0;
970
+ setIsDragging(false);
971
+ if (!event.dataTransfer || !isFileDrag(event.dataTransfer)) return;
972
+ const handledByDropzone = event.defaultPrevented;
973
+ event.preventDefault();
974
+ if (handledByDropzone) return;
975
+ getDroppedFiles(event.dataTransfer).then((files) => {
976
+ if (files.length) onDropRef.current(files);
977
+ });
978
+ }
979
+ function onBlur() {
980
+ depth = 0;
981
+ setIsDragging(false);
982
+ }
983
+ window.addEventListener("dragenter", onDragEnter);
984
+ window.addEventListener("dragover", onDragOver);
985
+ window.addEventListener("dragleave", onDragLeave);
986
+ window.addEventListener("drop", onDropEvent);
987
+ window.addEventListener("blur", onBlur);
988
+ return () => {
989
+ window.removeEventListener("dragenter", onDragEnter);
990
+ window.removeEventListener("dragover", onDragOver);
991
+ window.removeEventListener("dragleave", onDragLeave);
992
+ window.removeEventListener("drop", onDropEvent);
993
+ window.removeEventListener("blur", onBlur);
994
+ setIsDragging(false);
995
+ };
996
+ }, [enabled]);
997
+ return { isDragging: enabled && isDragging };
998
+ }
999
+ //#endregion
1000
+ //#region src/use-paste-files.ts
1001
+ /**
1002
+ * Uploads files and screenshots pasted from the clipboard. Text pastes are
1003
+ * left alone so inputs keep working normally.
1004
+ */
1005
+ function usePasteFiles({ onPaste, target, enabled = true }) {
1006
+ const onPasteRef = React.useRef(onPaste);
1007
+ React.useEffect(() => {
1008
+ onPasteRef.current = onPaste;
1009
+ });
1010
+ React.useEffect(() => {
1011
+ if (!enabled) return;
1012
+ const element = target?.current ?? document;
1013
+ function handlePaste(event) {
1014
+ const files = getClipboardFiles(event.clipboardData);
1015
+ if (files.length === 0) return;
1016
+ event.preventDefault();
1017
+ onPasteRef.current(files);
1018
+ }
1019
+ element.addEventListener("paste", handlePaste);
1020
+ return () => element.removeEventListener("paste", handlePaste);
1021
+ }, [enabled, target]);
1022
+ }
1023
+ //#endregion
1024
+ //#region src/use-file-validation.ts
1025
+ /**
1026
+ * Standalone validation for flows that don't upload immediately — e.g.
1027
+ * collecting files in a form and submitting them later. Uses the same rules
1028
+ * as the uploader, including async and custom validators.
1029
+ */
1030
+ function useFileValidation(options) {
1031
+ const [isValidating, setIsValidating] = React.useState(false);
1032
+ const optionsRef = React.useRef(options);
1033
+ React.useEffect(() => {
1034
+ optionsRef.current = options;
1035
+ });
1036
+ const pending = React.useRef(null);
1037
+ React.useEffect(() => () => pending.current?.abort(), []);
1038
+ return {
1039
+ validate: React.useCallback(async (files, context = {}) => {
1040
+ pending.current?.abort();
1041
+ const controller = new AbortController();
1042
+ pending.current = controller;
1043
+ setIsValidating(true);
1044
+ try {
1045
+ return await validateFiles(files, optionsRef.current, {
1046
+ ...context,
1047
+ signal: controller.signal
1048
+ });
1049
+ } finally {
1050
+ if (pending.current === controller) {
1051
+ pending.current = null;
1052
+ setIsValidating(false);
1053
+ }
1054
+ }
1055
+ }, []),
1056
+ validateCount: React.useCallback((count) => validateFileCount(count, optionsRef.current), []),
1057
+ isValidating
1058
+ };
1059
+ }
1060
+ //#endregion
1061
+ //#region src/use-image.ts
1062
+ /** Compresses images on demand and reports how much was saved. */
1063
+ function useImageCompression(options = {}) {
1064
+ const [isCompressing, setIsCompressing] = React.useState(false);
1065
+ const [stats, setStats] = React.useState(null);
1066
+ const [error, setError] = React.useState(null);
1067
+ const optionsRef = React.useRef(options);
1068
+ React.useEffect(() => {
1069
+ optionsRef.current = options;
1070
+ });
1071
+ return {
1072
+ compress: React.useCallback(async (file) => {
1073
+ setIsCompressing(true);
1074
+ setError(null);
1075
+ try {
1076
+ const result = await compressImage(file, optionsRef.current);
1077
+ setStats({
1078
+ before: file.size,
1079
+ after: result.size,
1080
+ saved: file.size === 0 ? 0 : 1 - result.size / file.size
1081
+ });
1082
+ return result;
1083
+ } catch (cause) {
1084
+ const failure = cause instanceof Error ? cause : new Error(String(cause));
1085
+ setError(failure);
1086
+ throw failure;
1087
+ } finally {
1088
+ setIsCompressing(false);
1089
+ }
1090
+ }, []),
1091
+ isCompressing,
1092
+ stats,
1093
+ error
1094
+ };
1095
+ }
1096
+ const INITIAL_CROP = {
1097
+ position: {
1098
+ x: 0,
1099
+ y: 0
1100
+ },
1101
+ zoom: 1,
1102
+ rotation: 0,
1103
+ flipHorizontal: false,
1104
+ flipVertical: false,
1105
+ area: null
1106
+ };
1107
+ /**
1108
+ * UI-agnostic crop/rotate/zoom state with an `apply` that renders the
1109
+ * result. Pair it with any crop UI (react-easy-crop, a canvas, sliders).
1110
+ */
1111
+ function useImageCrop({ minZoom = 1, maxZoom = 4 } = {}) {
1112
+ const [state, setState] = React.useState(INITIAL_CROP);
1113
+ const actions = React.useMemo(() => ({
1114
+ setPosition: (position) => setState((s) => ({
1115
+ ...s,
1116
+ position
1117
+ })),
1118
+ setZoom: (zoom) => setState((s) => ({
1119
+ ...s,
1120
+ zoom: Math.min(maxZoom, Math.max(minZoom, zoom))
1121
+ })),
1122
+ setRotation: (rotation) => setState((s) => ({
1123
+ ...s,
1124
+ rotation: (rotation % 360 + 360) % 360
1125
+ })),
1126
+ rotateBy: (degrees) => setState((s) => ({
1127
+ ...s,
1128
+ rotation: ((s.rotation + degrees) % 360 + 360) % 360
1129
+ })),
1130
+ flip: (axis) => setState((s) => axis === "horizontal" ? {
1131
+ ...s,
1132
+ flipHorizontal: !s.flipHorizontal
1133
+ } : {
1134
+ ...s,
1135
+ flipVertical: !s.flipVertical
1136
+ }),
1137
+ setArea: (area) => setState((s) => ({
1138
+ ...s,
1139
+ area
1140
+ })),
1141
+ reset: () => setState(INITIAL_CROP)
1142
+ }), [minZoom, maxZoom]);
1143
+ const apply = React.useCallback((file, options = {}) => transformImage(file, {
1144
+ ...options,
1145
+ crop: state.area ?? void 0,
1146
+ rotate: state.rotation,
1147
+ flipHorizontal: state.flipHorizontal,
1148
+ flipVertical: state.flipVertical
1149
+ }), [state]);
1150
+ return {
1151
+ ...state,
1152
+ ...actions,
1153
+ minZoom,
1154
+ maxZoom,
1155
+ apply
1156
+ };
1157
+ }
1158
+ //#endregion
1159
+ //#region src/use-network.ts
1160
+ function subscribe(callback) {
1161
+ window.addEventListener("online", callback);
1162
+ window.addEventListener("offline", callback);
1163
+ return () => {
1164
+ window.removeEventListener("online", callback);
1165
+ window.removeEventListener("offline", callback);
1166
+ };
1167
+ }
1168
+ /**
1169
+ * `navigator.onLine`, kept in sync with `online`/`offline` events. `true`
1170
+ * on the server. Note that "online" only means a network interface is up.
1171
+ */
1172
+ function useNetworkStatus() {
1173
+ return React.useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
1174
+ }
1175
+ /**
1176
+ * Warns before closing or reloading the tab while uploads are in progress.
1177
+ * Browsers show their own generic message; custom text is not supported.
1178
+ */
1179
+ function useUploadGuard(uploader, { enabled = true } = {}) {
1180
+ const isUploading = useUploadSelector(uploader, (state) => getUploadSummary(state.items).isUploading);
1181
+ React.useEffect(() => {
1182
+ if (!enabled || !isUploading) return;
1183
+ function onBeforeUnload(event) {
1184
+ event.preventDefault();
1185
+ }
1186
+ window.addEventListener("beforeunload", onBeforeUnload);
1187
+ return () => window.removeEventListener("beforeunload", onBeforeUnload);
1188
+ }, [enabled, isUploading]);
1189
+ return isUploading;
1190
+ }
1191
+ //#endregion
1192
+ //#region src/index.ts
1193
+ /**
1194
+ * Headless, accessible upload primitives. Style them directly or use the
1195
+ * shadcn-styled versions from the UploadCN registry.
1196
+ */
1197
+ const Upload = {
1198
+ Root: UploadRoot,
1199
+ Dropzone: UploadDropzone,
1200
+ Trigger: UploadTrigger,
1201
+ List: UploadList,
1202
+ Item: UploadItemRoot,
1203
+ Preview: UploadPreview,
1204
+ Name: UploadName,
1205
+ Size: UploadSize,
1206
+ Speed: UploadSpeed,
1207
+ Status: UploadStatus,
1208
+ Error: UploadError,
1209
+ Success: UploadSuccess,
1210
+ Progress: UploadProgress,
1211
+ ProgressIndicator: UploadProgressIndicator,
1212
+ Remove: UploadRemove,
1213
+ Cancel: UploadCancel,
1214
+ Retry: UploadRetry,
1215
+ Pause: UploadPause,
1216
+ Resume: UploadResume,
1217
+ Empty: UploadEmpty,
1218
+ Start: UploadStart,
1219
+ Clear: UploadClear
1220
+ };
1221
+ //#endregion
1222
+ export { Upload, UploadCancel, UploadClear, UploadConfigProvider, UploadContext, UploadDropzone, UploadEmpty, UploadError, UploadItemContext, UploadItemRoot, UploadList, UploadName, UploadPause, UploadPreview, UploadProgress, UploadProgressIndicator, UploadRemove, UploadResume, UploadRetry, UploadRoot, UploadSize, UploadSpeed, UploadStart, UploadStatus, UploadSuccess, UploadTrigger, defaultMessages, getPreviewKind, getStatusLabel, mergeProps, shallowArrayEqual, useDropzone, useFilePreview, useFileValidation, useImageCompression, useImageCrop, useNetworkStatus, usePasteFiles, useUpload, useUploadConfig, useUploadContext, useUploadGuard, useUploadItem, useUploadProgress, useUploadSelector, useUploadState, useUploadValue, useUploader, useWindowDrop };
1223
+
1224
+ //# sourceMappingURL=index.js.map