The tray collapses from its header and dismisses with the X.
The tray collapses from its header and dismisses with the X.
npx shadcn@latest add https://sdk-components.thesqd.com/r/floating-upload-progress.jsonimport { FloatingUploadProgress } from "@/components/blocks/floating-upload-progress";import * as React from "react";
import { FileUploadV2 } from "@/components/blocks/file-upload-v2";
import { FloatingUploadProgress } from "@/components/blocks/floating-upload-progress";
import type { MotionUploadItem } from "@/components/blocks/file-upload-motion";
export function Uploader() {
const [items, setItems] = React.useState<MotionUploadItem[]>([]);
const upsert = (id: string, patch: Partial<MotionUploadItem>) =>
setItems((cur) =>
cur.some((i) => i.id === id)
? cur.map((i) => (i.id === id ? { ...i, ...patch } : i))
: [...cur, { id, name: "file", kind: "file", status: "idle", ...patch } as MotionUploadItem],
);
return (
<>
<FileUploadV2
label="Assets"
onEvent={(e) => {
switch (e.type) {
case "com.thesqd.file-upload.upload.progress": {
const { file, progress } = e.data;
const pct = progress.percent ?? 0;
upsert(file.id, {
name: file.name ?? "file",
size: file.size,
kind: file.mimeType?.startsWith("image/") ? "image" : "file",
status: "uploading",
progress: pct,
statusText: `Uploading ${Math.round(pct)}%`,
});
break;
}
case "com.thesqd.file-upload.file.stored":
upsert(e.data.file.id, {
status: "complete",
progress: 100,
statusText: "Complete",
href: e.data.url,
});
break;
case "com.thesqd.file-upload.upload.failed":
upsert(e.data.file.id, { status: "failed", error: e.data.error.message });
break;
case "com.thesqd.file-upload.file.removed":
setItems((cur) => cur.filter((i) => i.id !== e.data.file.id));
break;
}
}}
/>
<FloatingUploadProgress
items={items}
onCancel={(item) => setItems((cur) => cur.filter((i) => i.id !== item.id))}
/>
</>
);
}{
"id": "uppy-sunday/recap-1e",
"name": "Sunday-recap-final.mp4",
"size": 148000000,
"kind": "video",
"status": "uploading",
"progress": 42,
"statusText": "Uploading 42%"
}FloatingUploadProgress — the floating tray (portals to <body> when anchor="fixed")
header — count label + optional action button + collapse caret + dismiss X
rows — one per item: thumb/kind icon, name, status, Progress bar
FloatingUploadProgressDemo — preview with a fake uploader
FloatingUploadProgressFailedDemo — preview where the last file fails and can retry| Prop | Type | Default | Description |
|---|---|---|---|
items | MotionUploadItem[] | — | Rows to show — the same shape FileUploadV2 feeds its motion skin (id, name, kind, status, progress, statusText, error, previewUrl, href). |
open | boolean | — | Controlled visibility. Defaults to visible whenever items is non-empty. |
onOpenChange | (open: boolean) => void | — | Fires with the next open state; the header X sets it false. |
defaultCollapsed | boolean | false | Start with the header only, rows collapsed. |
onCancel | (item: MotionUploadItem) => void | — | Per-row X. Omit for a read-only tray. |
onRetry | (item: MotionUploadItem) => void | — | Shown on failed rows only. Omit to render a static warning glyph instead. |
title | ReactNode | — | Header label. Defaults to "Uploading 3 items" → "1 item failed" → "3 uploads complete". |
action | { label: string; href?: string; onClick?: () => void; icon?: ReactNode } | — | Optional third header button, left of collapse/close — links to (href) or fires (onClick) the full uploader page. Defaults to an open-in-new arrow icon. |
anchor | "fixed" | "absolute" | "fixed" | "fixed" portals to <body> (bottom-right of the viewport); "absolute" anchors to the nearest positioned parent. |
className | string | — | Extra classes on the tray panel — override the position or width here. |