Floating upload progress

A floating tray that reports in-flight uploads, one progress row per file.

Preview
Start the upload to fill the tray. The header strip collapses the rows, each row cancels with its X, and the tray dismisses with the header X — a fresh batch brings it back. Passing `action` adds a third header button, left of the other two, that links out to the full uploader.

The tray collapses from its header and dismisses with the X.

Failure and retry
The last file fails. A failed row swaps its bar for the error text and shows a retry button when onRetry is passed; the header switches to "1 item failed".

The tray collapses from its header and dismisses with the X.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/floating-upload-progress.json
Usage
Fold FileUploadV2's onEvent stream into MotionUploadItem[] and hand it to the tray. The default fixed anchor portals to the document body, so it never sits inside a form.
TSImport
import { FloatingUploadProgress } from "@/components/blocks/floating-upload-progress";
TSExample
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))}
      />
    </>
  );
}
Item shape
One entry of the items array — MotionUploadItem, shared with FileUploadV2's motion skin.
{
  "id": "uppy-sunday/recap-1e",
  "name": "Sunday-recap-final.mp4",
  "size": 148000000,
  "kind": "video",
  "status": "uploading",
  "progress": 42,
  "statusText": "Uploading 42%"
}
Composition
One block export plus the two previews.
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
API Reference
Props for FloatingUploadProgress.
PropTypeDefaultDescription
itemsMotionUploadItem[]—Rows to show — the same shape FileUploadV2 feeds its motion skin (id, name, kind, status, progress, statusText, error, previewUrl, href).
openboolean—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.
defaultCollapsedbooleanfalseStart 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.
titleReactNode—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.
classNamestring—Extra classes on the tray panel — override the position or width here.