## Requirements

* A modern browser with `XMLHttpRequest` (the default transport uses it for byte-level upload
  progress).
* For the React binding: React 19.

## Install

```package-install
@gentleduck/upload
```

Or with a specific manager:

```bash tab="bun"
bun add @gentleduck/upload
```

```bash tab="npm"
npm install @gentleduck/upload
```

```bash tab="pnpm"
pnpm add @gentleduck/upload
```

One package ships everything. The core engine, React bindings, and built-in strategies are
reachable through three subpath entry points.

## Entry points

| Import | Contains |
| --- | --- |
| `@gentleduck/upload` | Re-exports all three below |
| `@gentleduck/upload/core` | `createUploadStore`, `Contracts`, `Engine`, error classes, persistence adapters, `createXHRTransport` |
| `@gentleduck/upload/strategies` | `PostStrategy`, `multipartStrategy`, `createStrategyRegistry` |
| `@gentleduck/upload/react` | `UploadProvider`, `useUploader`, `useUploaderActions` |

## Four steps to a working store

### 1. Describe your pipeline

The store is generic over `<M, C, P, R>` (intent map, cursor map, purpose, result). Define
them once and reuse everywhere:

```ts
import type { Contracts } from '@gentleduck/upload/core'
import type { PostStrategy } from '@gentleduck/upload/strategies'

type Intents = { post: PostStrategy.Intent }
type Cursors = { post?: PostStrategy.Cursor }
type Purpose = 'avatar' | 'document'
type Result = Contracts.Result.Base & { url: string }
```

`Contracts.Result.Base` is `{ fileId: string; key: string }` — extend it with whatever your
backend returns.

### 2. Implement the backend contract

Your adapter implements `Contracts.Api.Me<M, P, R>`. Only `createIntent` and `complete` are
required. Note the second argument is the call **context** (`ctx`), which carries the abort
`signal`, the `File`, the fingerprint, and retry metadata.

```ts
const api: Contracts.Api.Me<Intents, Purpose, Result> = {
  async createIntent({ purpose, contentType, size, filename }, ctx) {
    const res = await fetch('/api/uploads/intent', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ purpose, contentType, size, filename }),
      signal: ctx.signal,
    })
    if (!res.ok) throw new Error(`intent failed: ${res.status}`)
    // Returns a value from the intent map — here a PostStrategy.Intent.
    return res.json()
  },
  async complete({ fileId, filename, contentType, size }, ctx) {
    const res = await fetch(`/api/uploads/${fileId}/complete`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ filename, contentType, size }),
      signal: ctx.signal,
    })
    if (!res.ok) throw new Error(`complete failed: ${res.status}`)
    return res.json()
  },
}
```

### 3. Register strategies

`createStrategyRegistry()` returns an **empty** typed registry. Add strategies with `.set()`:

```ts
import { PostStrategy, createStrategyRegistry } from '@gentleduck/upload/strategies'

const strategies = createStrategyRegistry<Intents, Cursors, Purpose, Result>()
strategies.set(PostStrategy())
```

### 4. Create the store

```ts
import { createUploadStore } from '@gentleduck/upload/core'

export const store = createUploadStore<Intents, Cursors, Purpose, Result>({
  api,
  strategies,
  config: {
    maxConcurrentUploads: 3,
    autoStart: ['avatar'],
  },
})
```

If you omit `transport`, the store installs `createXHRTransport()` for you.

## Add React (optional)

Wrap your tree in `UploadProvider` and read the store with `useUploader`:

```tsx
import { UploadProvider, useUploader } from '@gentleduck/upload/react'
import { store } from './upload'

export function App() {
  return (
    <UploadProvider store={store}>
      <UploadList />
    </UploadProvider>
  )
}

function UploadList() {
  const { items, dispatch } = useUploader<Intents, Cursors, Purpose, Result>()

  return (
    <div>
      <input
        type="file"
        multiple
        onChange={(e) =>
          dispatch({ type: 'addFiles', files: Array.from(e.target.files ?? []), purpose: 'document' })
        }
      />
      {items.map((item) => (
        <div key={item.localId}>
          {item.fingerprint.name} — {item.phase}
        </div>
      ))}
    </div>
  )
}
```

## Next

* [Design Decisions](/duck-upload/design) — why the engine is shaped this way.
* [Core Overview](/duck-upload/core) — the modules behind the store.
* [Guides](/duck-upload/guides) — copy-paste recipes for common scenarios.