# FileUpload

A área de anexar: clique abre o seletor, arrastar acende, soltar valida.

A peça não conhece rede, de propósito, como o `DataTable` não conhece React
Query. Subir o arquivo (fetch, progresso real, nova tentativa) é do app, que
sabe o endpoint e a autenticação. A peça valida `accept` e `maxSize` na
entrada, entrega os aceitos em `onSelect` e os recusados em `onReject`, cada
recusa com o motivo pronto para um toast: "maior que 5 MB", "tipo não aceito".

A lista é apresentação do estado que o app informar: `progress` de 0 a 100
vira barra anunciada como `progressbar`; `error` vence o progresso, mostra o
texto e oferece "Tentar de novo"; sem os dois, o arquivo está pronto. O
tamanho sai formatado pela peça (`48,2 KB`, `1,2 MB`), nunca digitado.

A área é um `<button>` de verdade, então teclado e leitor de tela funcionam
sem esforço; o `<input type="file">` escondido carrega `accept` e `multiple`,
e o diálogo do sistema já filtra os tipos.

Quem informa o formato e o limite no `hint` evita a recusa: a pessoa lê "XML
ou PDF, até 5 MB" antes de escolher errado.

## As partes

`FileUploadList` é a lista do que já entrou, e `FileUploadItem` é cada arquivo
nela, com nome, tamanho, `progress` e `error`. A barra de cada arquivo anda até
o valor novo pela largura, no tempo `--rc-duration-base`, em vez de pular de
um número para o outro a cada aviso do envio. Subir é do app: a peça valida na
entrada e mostra o que o app disser depois. Quem controla o envio é quem sabe
quando ele terminou.

## Movimento

Cada `FileUploadItem` entra esmaecendo e subindo 4px (`animate-enter`, `--rc-duration-base`), então o arquivo recém-escolhido é visto chegando à lista. A barra de envio anda pela largura.

## No React Native

Traduz, no caminho próprio `@rivocode/ui-native/file-upload`: o `expo-document-picker` é peer **opcional** e módulo nativo (`npx expo install expo-document-picker`), e tem caminho separado do `Clipboard` pela mesma conta: a regra da casa é **um subcaminho por peer**, e não um por assunto. O que não muda é o principal: **a peça continua não conhecendo rede**. Ela valida `accept` e `maxSize` na entrada, entrega os aceitos em `onSelect` e os recusados em `onReject`, cada recusa com o motivo pronto para um aviso.

**A área de soltar vira um botão, e isso é a peça inteira mudando de forma.** No celular não há arrastar: nada pode ser solto em lugar nenhum, e o retângulo tracejado de 96px do web é, letra por letra, o idioma de "solte aqui": desenhá-lo numa tela de toque promete um gesto que o aparelho não tem. Tirado o soltar, o que sobra daquela caixa é um botão com muito espaço vazio em volta: **o espaço era o alvo de soltar, e não a affordance**. Então sobra o botão, numa altura de controle. E a altura que ele devolve é da **lista**, que é onde o arquivo aparece, sobe, falha e é removido. O `hint` continua existindo, e entra no nome falado do botão pelo mesmo motivo que no web ele mora dentro do `<button>`: quem ouve a tela precisa saber "XML ou PDF, até 5 MB" antes de abrir o seletor, e não depois de ser recusado.

**O `accept` fala MIME.** O seletor do Expo filtra por tipo (`text/xml`, `image/*`), e não por extensão: um `.xml` mandado para lá não casaria nada e abriria o diálogo vazio. Então a extensão com ponto continua valendo (na validação de volta, contra o nome do arquivo), mas não vai para o sistema. E o que volta não é um `File`: é um `PickedFile` (`uri`, `name`, `size?`, `mimeType?`), com o `uri` local que o app usa para subir. **O `size` pode faltar**, porque nem todo provedor de arquivo do Android o informa, e por isso `maxSize` só recusa o que conseguiu medir. Fechar o seletor devolve `canceled` e nenhum callback dispara, como fechar a janela do seletor do web.

`FileUploadList` e `FileUploadItem` atravessam com o mesmo contrato (`progress` de 0 a 100 vira barra anunciada, `error` vence o progresso e oferece "Tentar de novo"), com duas diferenças de plataforma: o corte do nome é `numberOfLines`, que lá é prop e não classe, e o tamanho sai formatado **sem `Intl`** ("47,1 KB", com a vírgula escrita à mão), pela mesma razão que o `Meter` nativo não tem `format`.

## Importação

```tsx
import { FileUpload } from '@rivocode/ui'
```

## Exemplos

### Padrão

```tsx
import {
  FileUpload,
  FileUploadItem,
  FileUploadList,
  useToast,
  type Rejection,
} from '@rivocode/ui'
import { useState } from 'react'

type Item = {
  id: string
  name: string
  size: number
  progress?: number
  error?: string
}

export function Default() {
  return (
    <FileUpload
      className="w-full max-w-md"
      label="Arraste o XML da nota, ou clique para escolher"
      hint="XML ou PDF, até 5 MB"
      accept=".xml,application/pdf"
      maxSize={5 * 1024 * 1024}
      multiple
    />
  )
}

type Item = {
  id: string
  name: string
  size: number
  progress?: number
  error?: string
}
```

### Com a lista de enviados

```tsx
import {
  FileUpload,
  FileUploadItem,
  FileUploadList,
  useToast,
  type Rejection,
} from '@rivocode/ui'
import { useState } from 'react'

type Item = {
  id: string
  name: string
  size: number
  progress?: number
  error?: string
}

export function WithList() {
  const toast = useToast()
  const [items, setItems] = useState<Item[]>([
    { id: '1', name: 'nota-4813.xml', size: 48_213 },
    { id: '2', name: 'comprovante-agosto.pdf', size: 1_284_500, progress: 62 },
    { id: '3', name: 'contrato-prefeitura.pdf', size: 3_410_000, error: 'A conexão caiu.' },
  ])
```

### Desabilitada

```tsx
import {
  FileUpload,
  FileUploadItem,
  FileUploadList,
  useToast,
  type Rejection,
} from '@rivocode/ui'
import { useState } from 'react'

export function Disabled() {
  return (
    <FileUpload
      className="w-full max-w-md"
      label="Arraste o XML da nota"
      hint="Envio bloqueado enquanto a emissão está em andamento."
      disabled
    />
  )
}
```

## Props

| Prop | Tipo | Obrigatória | Desde | O que faz |
| --- | --- | --- | --- | --- |
| `label` | `ReactNode` | sim | 0.4.0 | A frase da área: "Arraste o XML da nota, ou clique para escolher". |
| `accept` | `string` |  | 0.4.0 | Como no seletor nativo: `.xml,application/pdf`, `image/*`. |
| `disabled` | `boolean` |  | 0.4.0 |  |
| `hint` | `ReactNode` |  | 0.4.0 | A letra miúda: formatos e limite, para a pessoa não descobrir na recusa. |
| `maxSize` | `number` |  | 0.4.0 | Em bytes. |
| `multiple` | `boolean` |  | 0.4.0 |  |
| `onReject` | `((rejections: Rejection[]) => void)` |  | 0.4.0 | Os que não passaram, cada um com o motivo legível. |
| `onSelect` | `((files: File[]) => void)` |  | 0.4.0 | Os que passaram na validação. |

Além dessas: repassa `className`, `style`, `id` e os demais atributos do elemento raiz.

## Ver também

- [Autocomplete](/componentes/autocomplete.md)
- [Calendar](/componentes/calendar.md)
- [Checkbox](/componentes/checkbox.md)
- [CheckboxGroup](/componentes/checkbox-group.md)
- [ColorPicker](/componentes/color-picker.md)
- [Combobox](/componentes/combobox.md)
- [Convenções da biblioteca](/convencoes.md): Provider, tokens e as regras que valem para toda peça
- [Índice completo](/llms.txt)
