# File share

> Drop files, watch them upload, and they fold into one share link with an expiry and an optional password.

Most share screens are an upload field, a list with spinners and a link in a modal at the end. Here the lit panel is the drop area: drag files over it and its edge brightens and says how many will land. Each file uploads the moment it’s added, its row filling with a hairline as the percent rolls; a dropped connection offers Retry and removing a file aborts its upload. Choose how long the link lives and switch on a password. Create link goes Creating link → the rows lift and fly up into the panel, which turns into the link card: the URL with Copy, what’s in it, when it expires, and the files fanned as the stack they folded into. Storage is yours: onUpload sends one file with progress and an abort signal, onCreateLink makes the link.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/file-share.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: `lucide-react`
- Hairline UI components it needs (the CLI installs them too): `copy-button`, `text-morph`, `input`, `number-roll`, `segmented-control`, `status-button`, `switch`
- Docs and live preview: https://hairlineui.com/ui/file-share
- Registry item (source): https://hairlineui.com/r/file-share.json

## Usage

```tsx
import { FileShare } from "@/components/file-share";

<FileShare
  onUpload={(file, { onProgress, signal }) => uploadTo(storage, file, onProgress, signal)}
  onCreateLink={({ files, expiresIn, password }) => api.createShare(files, expiresIn, password)}
/>
```

## With presigned uploads

Ask your server for a presigned URL per file and PUT it with XMLHttpRequest, which reports progress; then make the link from the keys. Works with S3, R2, GCS or anything that signs URLs.

```tsx
"use client";
import { FileShare } from "@/components/file-share";

function put(url: string, file: File, onProgress: (f: number) => void, signal: AbortSignal) {
  return new Promise<void>((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("PUT", url);
    xhr.upload.onprogress = (e) => e.lengthComputable && onProgress(e.loaded / e.total);
    xhr.onload = () => (xhr.status < 300 ? resolve() : reject(new Error("Upload failed")));
    xhr.onerror = () => reject(new Error("Connection dropped"));
    signal.addEventListener("abort", () => xhr.abort());
    xhr.send(file);
  });
}

export function ShareFiles() {
  return (
    <FileShare
      maxSize={2_000_000_000}
      onUpload={async (file, { onProgress, signal }) => {
        const { url, key } = await (await fetch("/api/uploads", { method: "POST", body: JSON.stringify({ name: file.name, type: file.type }) })).json();
        await put(url, file, onProgress, signal);
        return key;
      }}
      onCreateLink={async ({ files, expiresIn, password }) => {
        const res = await fetch("/api/shares", { method: "POST", body: JSON.stringify({ keys: files.map((f) => f.result), expiresIn, password }) });
        if (!res.ok) throw new Error("Couldn’t make the link.");
        return res.json(); // { url }
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `onUpload` | `(file, { onProgress, signal }) => Promise<unknown>` | – | Upload one file; call onProgress(0–1), honour signal, resolve with what you need later (a key). Throw to offer Retry. |
| `onCreateLink` | `({ files, expiresIn, password? }) => Promise<{ url }>` | – | files carry { name, size, type, result }; expiresIn is in days. |
| `accept / maxSize / maxFiles` | `string / number / number` | `— / — / 20` | File types, bytes per file and how many files. |
| `expiries` | `number[]` | `[1, 7, 30]` | Days the link can live, as a segmented choice. |
| `ref` | `Ref<FileShareHandle>` | – | { add(files) } to add files from a paste handler or your own button. |

## States

- **empty**: The lit panel with a hairline drop area: an upload glyph, “Drop files to share” and choose files.
- **drag**: Files over the block brighten the area’s edge, settle it to 0.985× and morph the line to “Release to add 3”.
- **uploading**: Rows with a type tile, name, size and a rolling percent over a hairline that fills; the header rolls “3 files · 55 MB” and the share sent.
- **retry**: A failed upload shows the reason in red with a Retry button; removing a file aborts it.
- **options**: Expires after 1, 7 or 30 days, and a Password switch whose field folds open; Create link waits for a password once it’s on.
- **link**: Create link → Creating link → the rows arc up into the panel (560ms, 70ms apart) and the panel turns to the link: count and size, expiry, the URL with Copy, and the files fanned as tiles.

## What it handles

A share page is usually a drop zone, an upload queue, a settings form and a link dialog, each with its own state. This block carries the files from drop to link on one surface, against your own storage.

- **Upload on drop**: Each file starts as it’s added, with progress from onUpload’s callback and an abort signal for removal.
- **Failures you can retry**: A failed upload keeps its row with the reason and a Retry; files over maxSize say so and never upload.
- **Totals as they go**: The file count and the share sent roll while everything uploads.
- **Expiry and password**: A segmented choice of days and a password switch that opens its field; both reach onCreateLink.
- **Files into a link**: Create link sends the rows flying into the panel, which becomes the link card with Copy.
- **Add from anywhere**: A ref with add(files) lets a paste handler or your own button add files.

## Replaces

a drop zone, an upload queue, progress bars, link expiry settings, a password option, a share link dialog

## Where it fits

- **Send files**: A WeTransfer-style page for clients to send or receive large files.
- **Deliverables**: An agency or freelancer handing over a project’s files with an expiry.
- **Support uploads**: Customers sharing logs or recordings with a password.
- **Internal drops**: A quick way to share builds or exports inside a team.

## Notes

- Choose files is a real button that opens the file picker; dropping works anywhere on the block.
- Each row names its file and status; added files and the ready link are announced from a status region, and errors are text beside the file.
- Removing a file aborts its upload through the signal; nothing is sent twice.
- The flight is the rows themselves animated up before they fold away; nothing is cloned and focus stays put.
- Its light comes from your theme: the panel mixes --primary-foreground into --primary, file tiles take tints from --chart-1 to --chart-5. No drop shadows, glow or blur.
- Built from Segmented control, Switch, Input, Copy button, Number roll, Status button and Text morph, which the CLI installs with it. No animation library; with reduced motion nothing flies and the panel changes in place.

License: MIT (https://hairlineui.com/license#components)
