@uploadista/react 0.0.20-beta.9 → 0.1.0-beta.5

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.
@@ -0,0 +1,879 @@
1
+ "use client";
2
+
3
+ import type { BrowserUploadInput } from "@uploadista/client-browser";
4
+ import type { UploadFile } from "@uploadista/core/types";
5
+ import { createContext, type ReactNode, useCallback, useContext } from "react";
6
+ import {
7
+ type DragDropState,
8
+ type UseDragDropReturn,
9
+ useDragDrop,
10
+ } from "../hooks/use-drag-drop";
11
+ import {
12
+ type MultiUploadState,
13
+ type UploadItem as UploadItemData,
14
+ useMultiUpload,
15
+ } from "../hooks/use-multi-upload";
16
+ import type {
17
+ UploadState,
18
+ UploadStatus as UploadStatusType,
19
+ } from "../hooks/use-upload";
20
+
21
+ // Re-export types for convenience
22
+ export type {
23
+ UploadState,
24
+ UploadStatusType as UploadStatus,
25
+ UploadItemData as UploadItem,
26
+ MultiUploadState,
27
+ };
28
+
29
+ // ============ UPLOAD CONTEXT ============
30
+
31
+ /**
32
+ * Context value provided by the Upload component root.
33
+ * Contains all upload state and actions.
34
+ */
35
+ export interface UploadContextValue {
36
+ /** Whether in multi-file mode */
37
+ mode: "single" | "multi";
38
+ /** Current multi-upload state (aggregate) */
39
+ state: MultiUploadState;
40
+ /** All upload items */
41
+ items: UploadItemData[];
42
+ /** Whether auto-start is enabled */
43
+ autoStart: boolean;
44
+
45
+ /** Add files to the upload queue */
46
+ addFiles: (files: BrowserUploadInput[]) => void;
47
+ /** Remove an item from the queue */
48
+ removeItem: (id: string) => void;
49
+ /** Start all pending uploads */
50
+ startAll: () => void;
51
+ /** Abort a specific upload by ID */
52
+ abortUpload: (id: string) => void;
53
+ /** Abort all active uploads */
54
+ abortAll: () => void;
55
+ /** Retry a specific failed upload by ID */
56
+ retryUpload: (id: string) => void;
57
+ /** Retry all failed uploads */
58
+ retryFailed: () => void;
59
+ /** Clear all completed uploads */
60
+ clearCompleted: () => void;
61
+ /** Clear all items and reset state */
62
+ clearAll: () => void;
63
+
64
+ /** Internal handler for files received from drop zone */
65
+ handleFilesReceived: (files: File[]) => void;
66
+ }
67
+
68
+ const UploadContext = createContext<UploadContextValue | null>(null);
69
+
70
+ /**
71
+ * Hook to access upload context from within an Upload component.
72
+ * @throws Error if used outside of an Upload component
73
+ */
74
+ export function useUploadContext(): UploadContextValue {
75
+ const context = useContext(UploadContext);
76
+ if (!context) {
77
+ throw new Error(
78
+ "useUploadContext must be used within an <Upload> component. " +
79
+ "Wrap your component tree with <Upload onSuccess={...}>",
80
+ );
81
+ }
82
+ return context;
83
+ }
84
+
85
+ // ============ UPLOAD ITEM CONTEXT ============
86
+
87
+ /**
88
+ * Context value for a specific upload item within an Upload.
89
+ */
90
+ export interface UploadItemContextValue {
91
+ /** Item ID */
92
+ id: string;
93
+ /** The file being uploaded */
94
+ file: BrowserUploadInput;
95
+ /** Current upload state */
96
+ state: UploadState;
97
+ /** Abort this upload */
98
+ abort: () => void;
99
+ /** Retry this upload */
100
+ retry: () => void;
101
+ /** Remove this item from the queue */
102
+ remove: () => void;
103
+ }
104
+
105
+ const UploadItemContext = createContext<UploadItemContextValue | null>(null);
106
+
107
+ /**
108
+ * Hook to access upload item context from within an Upload.Item component.
109
+ * @throws Error if used outside of an Upload.Item component
110
+ */
111
+ export function useUploadItemContext(): UploadItemContextValue {
112
+ const context = useContext(UploadItemContext);
113
+ if (!context) {
114
+ throw new Error(
115
+ "useUploadItemContext must be used within an <Upload.Item> component. " +
116
+ 'Wrap your component with <Upload.Item id="...">',
117
+ );
118
+ }
119
+ return context;
120
+ }
121
+
122
+ // ============ UPLOAD ROOT COMPONENT ============
123
+
124
+ /**
125
+ * Props for the Upload root component.
126
+ */
127
+ export interface UploadProps {
128
+ /** Whether to allow multiple file uploads (default: false) */
129
+ multiple?: boolean;
130
+ /** Maximum concurrent uploads (default: 3, only used in multi mode) */
131
+ maxConcurrent?: number;
132
+ /** Whether to auto-start uploads when files are received (default: true) */
133
+ autoStart?: boolean;
134
+ /** Metadata to attach to uploads */
135
+ metadata?: Record<string, string>;
136
+ /** Called when a single file upload succeeds (single mode) */
137
+ onSuccess?: (result: UploadFile) => void;
138
+ /** Called when an upload fails */
139
+ onError?: (error: Error, item?: UploadItemData) => void;
140
+ /** Called when all uploads complete (multi mode) */
141
+ onComplete?: (results: {
142
+ successful: UploadItemData[];
143
+ failed: UploadItemData[];
144
+ total: number;
145
+ }) => void;
146
+ /** Called when an individual upload starts */
147
+ onUploadStart?: (item: UploadItemData) => void;
148
+ /** Called on upload progress */
149
+ onProgress?: (
150
+ item: UploadItemData,
151
+ progress: number,
152
+ bytesUploaded: number,
153
+ totalBytes: number | null,
154
+ ) => void;
155
+ /** Children to render */
156
+ children: ReactNode;
157
+ }
158
+
159
+ /**
160
+ * Root component for file uploads.
161
+ * Provides context for all Upload sub-components.
162
+ * Supports both single-file and multi-file modes via the `multiple` prop.
163
+ *
164
+ * @example Single file upload
165
+ * ```tsx
166
+ * <Upload onSuccess={handleSuccess}>
167
+ * <Upload.DropZone accept="image/*">
168
+ * {({ isDragging, getRootProps, getInputProps }) => (
169
+ * <div {...getRootProps()}>
170
+ * <input {...getInputProps()} />
171
+ * {isDragging ? "Drop here" : "Drag or click"}
172
+ * </div>
173
+ * )}
174
+ * </Upload.DropZone>
175
+ * <Upload.Progress>
176
+ * {({ progress }) => <progress value={progress} max={100} />}
177
+ * </Upload.Progress>
178
+ * </Upload>
179
+ * ```
180
+ *
181
+ * @example Multi-file upload
182
+ * ```tsx
183
+ * <Upload multiple maxConcurrent={3} onComplete={handleComplete}>
184
+ * <Upload.DropZone>
185
+ * {(props) => ...}
186
+ * </Upload.DropZone>
187
+ * <Upload.Items>
188
+ * {({ items }) => items.map(item => (
189
+ * <Upload.Item key={item.id} id={item.id}>
190
+ * {({ file, state, abort, remove }) => (
191
+ * <div>{file.name}: {state.progress}%</div>
192
+ * )}
193
+ * </Upload.Item>
194
+ * ))}
195
+ * </Upload.Items>
196
+ * <Upload.StartAll>Upload All</Upload.StartAll>
197
+ * </Upload>
198
+ * ```
199
+ */
200
+ function UploadRoot({
201
+ multiple = false,
202
+ maxConcurrent = 3,
203
+ autoStart = true,
204
+ metadata,
205
+ onSuccess,
206
+ onError,
207
+ onComplete,
208
+ onUploadStart,
209
+ onProgress,
210
+ children,
211
+ }: UploadProps) {
212
+ const multiUpload = useMultiUpload({
213
+ maxConcurrent,
214
+ metadata,
215
+ onUploadStart,
216
+ onUploadProgress: onProgress,
217
+ onUploadSuccess: (_item, result) => {
218
+ // In single mode, call onSuccess directly
219
+ if (!multiple) {
220
+ onSuccess?.(result);
221
+ }
222
+ },
223
+ onUploadError: (item, error) => {
224
+ onError?.(error, item);
225
+ },
226
+ onComplete,
227
+ });
228
+
229
+ const handleFilesReceived = useCallback(
230
+ (files: File[]) => {
231
+ if (!multiple) {
232
+ // Single mode: clear existing and add new file
233
+ multiUpload.clearAll();
234
+ }
235
+ multiUpload.addFiles(files);
236
+ if (autoStart) {
237
+ // Use setTimeout to ensure state is updated before starting
238
+ setTimeout(() => multiUpload.startAll(), 0);
239
+ }
240
+ },
241
+ [multiple, autoStart, multiUpload],
242
+ );
243
+
244
+ const contextValue: UploadContextValue = {
245
+ mode: multiple ? "multi" : "single",
246
+ state: multiUpload.state,
247
+ items: multiUpload.items,
248
+ autoStart,
249
+ addFiles: multiUpload.addFiles,
250
+ removeItem: multiUpload.removeItem,
251
+ startAll: multiUpload.startAll,
252
+ abortUpload: multiUpload.abortUpload,
253
+ abortAll: multiUpload.abortAll,
254
+ retryUpload: multiUpload.retryUpload,
255
+ retryFailed: multiUpload.retryFailed,
256
+ clearCompleted: multiUpload.clearCompleted,
257
+ clearAll: multiUpload.clearAll,
258
+ handleFilesReceived,
259
+ };
260
+
261
+ return (
262
+ <UploadContext.Provider value={contextValue}>
263
+ {children}
264
+ </UploadContext.Provider>
265
+ );
266
+ }
267
+
268
+ // ============ DROP ZONE PRIMITIVE ============
269
+
270
+ /**
271
+ * Render props for Upload.DropZone component.
272
+ */
273
+ export interface UploadDropZoneRenderProps {
274
+ /** Whether files are being dragged over */
275
+ isDragging: boolean;
276
+ /** Whether drag is over the zone */
277
+ isOver: boolean;
278
+ /** Validation errors */
279
+ errors: string[];
280
+ /** Props to spread on the drop zone container */
281
+ getRootProps: () => UseDragDropReturn["dragHandlers"];
282
+ /** Props to spread on the hidden file input */
283
+ getInputProps: () => UseDragDropReturn["inputProps"];
284
+ /** Open file picker programmatically */
285
+ openFilePicker: () => void;
286
+ /** Current drag-drop state */
287
+ dragDropState: DragDropState;
288
+ }
289
+
290
+ /**
291
+ * Props for Upload.DropZone component.
292
+ */
293
+ export interface UploadDropZoneProps {
294
+ /** Accepted file types (e.g., "image/*", ".pdf") */
295
+ accept?: string;
296
+ /** Maximum file size in bytes */
297
+ maxFileSize?: number;
298
+ /** Maximum number of files (only in multi mode) */
299
+ maxFiles?: number;
300
+ /** Render function receiving drop zone state */
301
+ children: (props: UploadDropZoneRenderProps) => ReactNode;
302
+ }
303
+
304
+ /**
305
+ * Drop zone for file uploads within an Upload component.
306
+ * Handles drag-and-drop and click-to-select file selection.
307
+ *
308
+ * @example
309
+ * ```tsx
310
+ * <Upload.DropZone accept="image/*">
311
+ * {({ isDragging, getRootProps, getInputProps }) => (
312
+ * <div {...getRootProps()}>
313
+ * <input {...getInputProps()} />
314
+ * {isDragging ? "Drop here" : "Click or drag"}
315
+ * </div>
316
+ * )}
317
+ * </Upload.DropZone>
318
+ * ```
319
+ */
320
+ function UploadDropZone({
321
+ accept,
322
+ maxFileSize,
323
+ maxFiles,
324
+ children,
325
+ }: UploadDropZoneProps) {
326
+ const upload = useUploadContext();
327
+
328
+ const dragDrop = useDragDrop({
329
+ onFilesReceived: upload.handleFilesReceived,
330
+ accept: accept ? accept.split(",").map((t) => t.trim()) : undefined,
331
+ maxFileSize,
332
+ maxFiles: upload.mode === "multi" ? maxFiles : 1,
333
+ multiple: upload.mode === "multi",
334
+ });
335
+
336
+ const renderProps: UploadDropZoneRenderProps = {
337
+ isDragging: dragDrop.state.isDragging,
338
+ isOver: dragDrop.state.isOver,
339
+ errors: dragDrop.state.errors,
340
+ getRootProps: () => dragDrop.dragHandlers,
341
+ getInputProps: () => dragDrop.inputProps,
342
+ openFilePicker: dragDrop.openFilePicker,
343
+ dragDropState: dragDrop.state,
344
+ };
345
+
346
+ return <>{children(renderProps)}</>;
347
+ }
348
+
349
+ // ============ ITEMS PRIMITIVE ============
350
+
351
+ /**
352
+ * Render props for Upload.Items component.
353
+ */
354
+ export interface UploadItemsRenderProps {
355
+ /** All upload items */
356
+ items: UploadItemData[];
357
+ /** Whether there are any items */
358
+ hasItems: boolean;
359
+ /** Whether items array is empty */
360
+ isEmpty: boolean;
361
+ }
362
+
363
+ /**
364
+ * Props for Upload.Items component.
365
+ */
366
+ export interface UploadItemsProps {
367
+ /** Render function receiving items */
368
+ children: (props: UploadItemsRenderProps) => ReactNode;
369
+ }
370
+
371
+ /**
372
+ * Renders the list of upload items via render props.
373
+ *
374
+ * @example
375
+ * ```tsx
376
+ * <Upload.Items>
377
+ * {({ items, isEmpty }) => (
378
+ * isEmpty ? <p>No files</p> : (
379
+ * items.map(item => (
380
+ * <Upload.Item key={item.id} id={item.id}>
381
+ * {(props) => ...}
382
+ * </Upload.Item>
383
+ * ))
384
+ * )
385
+ * )}
386
+ * </Upload.Items>
387
+ * ```
388
+ */
389
+ function UploadItems({ children }: UploadItemsProps) {
390
+ const upload = useUploadContext();
391
+
392
+ const renderProps: UploadItemsRenderProps = {
393
+ items: upload.items,
394
+ hasItems: upload.items.length > 0,
395
+ isEmpty: upload.items.length === 0,
396
+ };
397
+
398
+ return <>{children(renderProps)}</>;
399
+ }
400
+
401
+ // ============ ITEM PRIMITIVE ============
402
+
403
+ /**
404
+ * Props for Upload.Item component.
405
+ */
406
+ export interface UploadItemProps {
407
+ /** Item ID */
408
+ id: string;
409
+ /** Children (can be render function or regular children) */
410
+ children: ReactNode | ((props: UploadItemContextValue) => ReactNode);
411
+ }
412
+
413
+ /**
414
+ * Scoped context provider for a specific upload item.
415
+ * Children can access item-specific state via useUploadItemContext().
416
+ *
417
+ * @example
418
+ * ```tsx
419
+ * <Upload.Item id={item.id}>
420
+ * {({ file, state, abort, remove }) => (
421
+ * <div>
422
+ * <span>{file.name}</span>
423
+ * <progress value={state.progress} max={100} />
424
+ * <button onClick={abort}>Cancel</button>
425
+ * <button onClick={remove}>Remove</button>
426
+ * </div>
427
+ * )}
428
+ * </Upload.Item>
429
+ * ```
430
+ */
431
+ function UploadItem({ id, children }: UploadItemProps) {
432
+ const upload = useUploadContext();
433
+
434
+ const item = upload.items.find((i) => i.id === id);
435
+
436
+ if (!item) {
437
+ // Item not found
438
+ return null;
439
+ }
440
+
441
+ const contextValue: UploadItemContextValue = {
442
+ id,
443
+ file: item.file,
444
+ state: item.state,
445
+ abort: () => upload.abortUpload(id),
446
+ retry: () => upload.retryUpload(id),
447
+ remove: () => upload.removeItem(id),
448
+ };
449
+
450
+ return (
451
+ <UploadItemContext.Provider value={contextValue}>
452
+ {typeof children === "function" ? children(contextValue) : children}
453
+ </UploadItemContext.Provider>
454
+ );
455
+ }
456
+
457
+ // ============ PROGRESS PRIMITIVE ============
458
+
459
+ /**
460
+ * Render props for Upload.Progress component.
461
+ */
462
+ export interface UploadProgressRenderProps {
463
+ /** Progress percentage (0-100) */
464
+ progress: number;
465
+ /** Bytes uploaded so far */
466
+ bytesUploaded: number;
467
+ /** Total bytes to upload */
468
+ totalBytes: number;
469
+ /** Whether any uploads are active */
470
+ isUploading: boolean;
471
+ }
472
+
473
+ /**
474
+ * Props for Upload.Progress component.
475
+ */
476
+ export interface UploadProgressProps {
477
+ /** Render function receiving progress state */
478
+ children: (props: UploadProgressRenderProps) => ReactNode;
479
+ }
480
+
481
+ /**
482
+ * Progress display component within an Upload.
483
+ *
484
+ * @example
485
+ * ```tsx
486
+ * <Upload.Progress>
487
+ * {({ progress, isUploading }) => (
488
+ * isUploading && <progress value={progress} max={100} />
489
+ * )}
490
+ * </Upload.Progress>
491
+ * ```
492
+ */
493
+ function UploadProgress({ children }: UploadProgressProps) {
494
+ const upload = useUploadContext();
495
+
496
+ const renderProps: UploadProgressRenderProps = {
497
+ progress: upload.state.progress,
498
+ bytesUploaded: upload.state.totalBytesUploaded,
499
+ totalBytes: upload.state.totalBytes,
500
+ isUploading: upload.state.isUploading,
501
+ };
502
+
503
+ return <>{children(renderProps)}</>;
504
+ }
505
+
506
+ // ============ STATUS PRIMITIVE ============
507
+
508
+ /**
509
+ * Render props for Upload.Status component.
510
+ */
511
+ export interface UploadStatusRenderProps {
512
+ /** Overall status */
513
+ status: "idle" | "uploading" | "success" | "error";
514
+ /** Whether idle (no uploads active or completed) */
515
+ isIdle: boolean;
516
+ /** Whether uploading */
517
+ isUploading: boolean;
518
+ /** Whether all uploads succeeded */
519
+ isSuccess: boolean;
520
+ /** Whether any upload failed */
521
+ isError: boolean;
522
+ /** Whether all uploads completed (success or failure) */
523
+ isComplete: boolean;
524
+ /** Number of total items */
525
+ total: number;
526
+ /** Number of successful uploads */
527
+ successful: number;
528
+ /** Number of failed uploads */
529
+ failed: number;
530
+ /** Number of currently uploading */
531
+ uploading: number;
532
+ }
533
+
534
+ /**
535
+ * Props for Upload.Status component.
536
+ */
537
+ export interface UploadStatusProps {
538
+ /** Render function receiving status state */
539
+ children: (props: UploadStatusRenderProps) => ReactNode;
540
+ }
541
+
542
+ /**
543
+ * Status display component within an Upload.
544
+ *
545
+ * @example
546
+ * ```tsx
547
+ * <Upload.Status>
548
+ * {({ status, total, successful, failed }) => (
549
+ * <div>
550
+ * Status: {status}
551
+ * ({successful}/{total} uploaded, {failed} failed)
552
+ * </div>
553
+ * )}
554
+ * </Upload.Status>
555
+ * ```
556
+ */
557
+ function UploadStatus({ children }: UploadStatusProps) {
558
+ const upload = useUploadContext();
559
+ const { state } = upload;
560
+
561
+ // Derive overall status
562
+ let status: "idle" | "uploading" | "success" | "error" = "idle";
563
+ if (state.isUploading) {
564
+ status = "uploading";
565
+ } else if (state.isComplete) {
566
+ status = state.failed > 0 ? "error" : "success";
567
+ }
568
+
569
+ const renderProps: UploadStatusRenderProps = {
570
+ status,
571
+ isIdle: status === "idle",
572
+ isUploading: state.isUploading,
573
+ isSuccess: state.isComplete && state.failed === 0,
574
+ isError: state.failed > 0,
575
+ isComplete: state.isComplete,
576
+ total: state.total,
577
+ successful: state.successful,
578
+ failed: state.failed,
579
+ uploading: state.uploading,
580
+ };
581
+
582
+ return <>{children(renderProps)}</>;
583
+ }
584
+
585
+ // ============ ERROR PRIMITIVE ============
586
+
587
+ /**
588
+ * Render props for Upload.Error component.
589
+ */
590
+ export interface UploadErrorRenderProps {
591
+ /** Whether there are any errors */
592
+ hasError: boolean;
593
+ /** Number of failed uploads */
594
+ failedCount: number;
595
+ /** Failed items */
596
+ failedItems: UploadItemData[];
597
+ /** Reset/clear all errors */
598
+ reset: () => void;
599
+ }
600
+
601
+ /**
602
+ * Props for Upload.Error component.
603
+ */
604
+ export interface UploadErrorProps {
605
+ /** Render function receiving error state */
606
+ children: (props: UploadErrorRenderProps) => ReactNode;
607
+ }
608
+
609
+ /**
610
+ * Error display component within an Upload.
611
+ *
612
+ * @example
613
+ * ```tsx
614
+ * <Upload.Error>
615
+ * {({ hasError, failedItems, reset }) => (
616
+ * hasError && (
617
+ * <div>
618
+ * {failedItems.map(item => (
619
+ * <p key={item.id}>{item.file.name}: {item.state.error?.message}</p>
620
+ * ))}
621
+ * <button onClick={reset}>Clear</button>
622
+ * </div>
623
+ * )
624
+ * )}
625
+ * </Upload.Error>
626
+ * ```
627
+ */
628
+ function UploadError({ children }: UploadErrorProps) {
629
+ const upload = useUploadContext();
630
+
631
+ const failedItems = upload.items.filter((item) =>
632
+ ["error", "aborted"].includes(item.state.status),
633
+ );
634
+
635
+ const renderProps: UploadErrorRenderProps = {
636
+ hasError: failedItems.length > 0,
637
+ failedCount: failedItems.length,
638
+ failedItems,
639
+ reset: upload.clearCompleted,
640
+ };
641
+
642
+ return <>{children(renderProps)}</>;
643
+ }
644
+
645
+ // ============ ACTION PRIMITIVES ============
646
+
647
+ /**
648
+ * Props for Upload.Cancel component.
649
+ */
650
+ export interface UploadCancelProps
651
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
652
+ /** Button content */
653
+ children: ReactNode;
654
+ }
655
+
656
+ /**
657
+ * Cancel button that aborts all active uploads.
658
+ * Automatically disabled when no uploads are active.
659
+ */
660
+ function UploadCancel({ children, disabled, ...props }: UploadCancelProps) {
661
+ const upload = useUploadContext();
662
+
663
+ const handleClick = useCallback(() => {
664
+ upload.abortAll();
665
+ }, [upload]);
666
+
667
+ return (
668
+ <button
669
+ type="button"
670
+ onClick={handleClick}
671
+ disabled={disabled || !upload.state.isUploading}
672
+ {...props}
673
+ >
674
+ {children}
675
+ </button>
676
+ );
677
+ }
678
+
679
+ /**
680
+ * Props for Upload.Retry component.
681
+ */
682
+ export interface UploadRetryProps
683
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
684
+ /** Button content */
685
+ children: ReactNode;
686
+ }
687
+
688
+ /**
689
+ * Retry button that retries all failed uploads.
690
+ * Automatically disabled when no failed uploads exist.
691
+ */
692
+ function UploadRetry({ children, disabled, ...props }: UploadRetryProps) {
693
+ const upload = useUploadContext();
694
+
695
+ const handleClick = useCallback(() => {
696
+ upload.retryFailed();
697
+ }, [upload]);
698
+
699
+ return (
700
+ <button
701
+ type="button"
702
+ onClick={handleClick}
703
+ disabled={disabled || upload.state.failed === 0}
704
+ {...props}
705
+ >
706
+ {children}
707
+ </button>
708
+ );
709
+ }
710
+
711
+ /**
712
+ * Props for Upload.Reset component.
713
+ */
714
+ export interface UploadResetProps
715
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
716
+ /** Button content */
717
+ children: ReactNode;
718
+ }
719
+
720
+ /**
721
+ * Reset button that clears all items and resets state.
722
+ */
723
+ function UploadReset({ children, ...props }: UploadResetProps) {
724
+ const upload = useUploadContext();
725
+
726
+ const handleClick = useCallback(() => {
727
+ upload.clearAll();
728
+ }, [upload]);
729
+
730
+ return (
731
+ <button type="button" onClick={handleClick} {...props}>
732
+ {children}
733
+ </button>
734
+ );
735
+ }
736
+
737
+ /**
738
+ * Props for Upload.StartAll component.
739
+ */
740
+ export interface UploadStartAllProps
741
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
742
+ /** Button content */
743
+ children: ReactNode;
744
+ }
745
+
746
+ /**
747
+ * Start all button that begins all queued uploads.
748
+ * Primarily useful when autoStart is disabled.
749
+ * Automatically disabled when uploads are already active.
750
+ */
751
+ function UploadStartAll({ children, disabled, ...props }: UploadStartAllProps) {
752
+ const upload = useUploadContext();
753
+
754
+ const handleClick = useCallback(() => {
755
+ upload.startAll();
756
+ }, [upload]);
757
+
758
+ // Count idle items
759
+ const idleCount = upload.items.filter(
760
+ (item) => item.state.status === "idle",
761
+ ).length;
762
+
763
+ return (
764
+ <button
765
+ type="button"
766
+ onClick={handleClick}
767
+ disabled={disabled || upload.state.isUploading || idleCount === 0}
768
+ {...props}
769
+ >
770
+ {children}
771
+ </button>
772
+ );
773
+ }
774
+
775
+ /**
776
+ * Props for Upload.ClearCompleted component.
777
+ */
778
+ export interface UploadClearCompletedProps
779
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
780
+ /** Button content */
781
+ children: ReactNode;
782
+ }
783
+
784
+ /**
785
+ * Clear completed button that removes all finished uploads from the list.
786
+ * Automatically disabled when no completed uploads exist.
787
+ */
788
+ function UploadClearCompleted({
789
+ children,
790
+ disabled,
791
+ ...props
792
+ }: UploadClearCompletedProps) {
793
+ const upload = useUploadContext();
794
+
795
+ const handleClick = useCallback(() => {
796
+ upload.clearCompleted();
797
+ }, [upload]);
798
+
799
+ return (
800
+ <button
801
+ type="button"
802
+ onClick={handleClick}
803
+ disabled={disabled || upload.state.completed === 0}
804
+ {...props}
805
+ >
806
+ {children}
807
+ </button>
808
+ );
809
+ }
810
+
811
+ // ============ COMPOUND COMPONENT EXPORT ============
812
+
813
+ /**
814
+ * Upload compound component for file uploads.
815
+ *
816
+ * Provides a composable, headless API for building upload interfaces.
817
+ * Supports both single-file and multi-file modes via the `multiple` prop.
818
+ * All sub-components use render props for complete UI control.
819
+ *
820
+ * @example Single file upload
821
+ * ```tsx
822
+ * <Upload onSuccess={handleSuccess}>
823
+ * <Upload.DropZone accept="image/*">
824
+ * {({ isDragging, getRootProps, getInputProps }) => (
825
+ * <div {...getRootProps()}>
826
+ * <input {...getInputProps()} />
827
+ * {isDragging ? "Drop here" : "Drag or click"}
828
+ * </div>
829
+ * )}
830
+ * </Upload.DropZone>
831
+ * <Upload.Progress>
832
+ * {({ progress }) => <progress value={progress} max={100} />}
833
+ * </Upload.Progress>
834
+ * </Upload>
835
+ * ```
836
+ *
837
+ * @example Multi-file upload
838
+ * ```tsx
839
+ * <Upload multiple maxConcurrent={3} onComplete={handleComplete}>
840
+ * <Upload.DropZone>
841
+ * {({ getRootProps, getInputProps }) => (
842
+ * <div {...getRootProps()}>
843
+ * <input {...getInputProps()} />
844
+ * Drop files here
845
+ * </div>
846
+ * )}
847
+ * </Upload.DropZone>
848
+ * <Upload.Items>
849
+ * {({ items }) => items.map(item => (
850
+ * <Upload.Item key={item.id} id={item.id}>
851
+ * {({ file, state, abort, remove }) => (
852
+ * <div>
853
+ * {file.name}: {state.progress}%
854
+ * <button onClick={abort}>Cancel</button>
855
+ * <button onClick={remove}>Remove</button>
856
+ * </div>
857
+ * )}
858
+ * </Upload.Item>
859
+ * ))}
860
+ * </Upload.Items>
861
+ * <Upload.StartAll>Upload All</Upload.StartAll>
862
+ * <Upload.Cancel>Cancel All</Upload.Cancel>
863
+ * <Upload.ClearCompleted>Clear Completed</Upload.ClearCompleted>
864
+ * </Upload>
865
+ * ```
866
+ */
867
+ export const Upload = Object.assign(UploadRoot, {
868
+ DropZone: UploadDropZone,
869
+ Items: UploadItems,
870
+ Item: UploadItem,
871
+ Progress: UploadProgress,
872
+ Status: UploadStatus,
873
+ Error: UploadError,
874
+ Cancel: UploadCancel,
875
+ Retry: UploadRetry,
876
+ Reset: UploadReset,
877
+ StartAll: UploadStartAll,
878
+ ClearCompleted: UploadClearCompleted,
879
+ });