# DropTarget

> Soltar archivos sobre cualquier contenido —una sección, una card— para abrir su formulario con el archivo, sin volverlo un recuadro de subida.

```tsx
import { DropTarget } from "sebs7n-ui/drop-target"
```

## Ejemplos

### Soltar sobre una sección

Arrastrá un PDF sobre la card de facturas: se pinta el anillo, y al soltar se «abre el formulario» con el archivo. El botón «Agregar factura» sigue andando, y es el camino sin arrastrar.

```tsx
import { Button } from "sebs7n-ui/button"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "sebs7n-ui/card"
import { DropTarget } from "sebs7n-ui/drop-target"
import { FileTextIcon, PlusIcon } from "lucide-react"
import { List, ListRow } from "sebs7n-ui/list-row"
import { useState } from "react"

function Section() {
  const [invoices, setInvoices] = useState(["factura-0012.pdf", "factura-0013.pdf"])
  const [draft, setDraft] = useState<string | null>(null)
  return (
    <DropTarget accept=".pdf" className="w-full max-w-md" maxSize={5 * 1024 * 1024} onDrop={([file]) => setDraft(file!.name)}>
      <Card>
        <CardHeader>
          <CardTitle>Facturas del cliente</CardTitle>
          <CardAction>
            <Button onClick={() => setDraft("")} size="sm" variant="secondary">
              <PlusIcon aria-hidden="true" />
              Agregar factura
            </Button>
          </CardAction>
        </CardHeader>
        <CardContent className="flex flex-col gap-3">
          <List>
            {invoices.map((name) => (
              <ListRow icon={<FileTextIcon />} key={name} title={name} />
            ))}
          </List>
          {draft !== null && (
            <div className="flex items-center justify-between gap-3 rounded-field bg-fill-1 px-3 py-2 text-callout text-label">
              <span className="truncate">{draft ? `Nueva factura con ${draft}` : "Nueva factura, sin archivo"}</span>
              <Button
                onClick={() => {
                  if (draft) setInvoices([...invoices, draft])
                  setDraft(null)
                }}
                size="sm"
              >
                Guardar
              </Button>
            </div>
          )}
        </CardContent>
      </Card>
    </DropTarget>
  )
}
```

## Props

### DropTarget

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `onDrop` * | `(files: File[]) => void` | — | Los archivos que pasaron `accept`, `maxSize` y `validate` (uno solo sin `multiple`). Lo usual: abrir el formulario con el archivo ya puesto. |
| `accept` | `string` | — | Los tipos que acepta, como el `accept` de un `<input type="file">`: `".pdf,image/*"`. |
| `disabled` | `boolean` | `false` | Apaga la interacción y lo marca con `data-disabled`, que es el atributo del que cuelgan los estilos de apagado. |
| `formatSize` | `(bytes: number) => string` | — | Cómo se escribe el tamaño en el error de `maxSize`. Por defecto, en base 1024 con `Intl`. |
| `labels` | `Partial<DropTargetLabels>` | — | Textos: `drop`, `added`, `invalidType`, `tooLarge`, `tooMany` y `locale`, los de `labels.dropZone`. |
| `maxSize` | `number` | — | El tamaño máximo de cada archivo, en bytes. |
| `multiple` | `boolean` | `false` | Más de un archivo por vez. Sin `multiple`, entra el primero y los demás se avisan. |
| `validate` | `(file: File) => string \| Promise<string>` | — | Una validación propia por archivo, como la de `DropZone`: el texto que devuelve es su error. Puede ser asíncrona; mientras tanto la zona lleva `aria-busy`, y sin `multiple` gana el último soltado. |
| `className` | `string` | — | Clases del `div` que envuelve (es `relative`). |

`*` obligatoria.

## Teclado

| Tecla | Qué hace |
|---|---|
| — | No agrega paradas de Tab: el contenido de adentro sigue con las suyas. El camino con teclado es el botón de la sección («Agregar factura»). |

## Accesibilidad

- No tiene rol ni foco: envuelve el contenido, que sigue siendo interactivo. Arrastrar es un atajo; **el contenido tiene que traer su botón** para el mismo resultado sin arrastrar (WCAG 2.5.7).
- El anillo y el tinte mientras se arrastra son decorativos (`aria-hidden`) y no reciben el puntero.
- Lo que entra se anuncia en una región viva («Archivos agregados: factura.pdf»), que se vacía antes de cada anuncio para que soltar el mismo archivo dos veces se lea dos veces; los rechazos aparecen abajo del contenido con `role="alert"`. Mientras corre un `validate` asíncrono lleva `aria-busy`.

## Reglas de uso

- Para una sección que ya tiene su «Agregar»: soltar un archivo encima abre el mismo formulario con el archivo puesto (`onDrop={([file]) => openForm(file)}`), y ahí el `DropZone` del formulario lo muestra. Para elegir y listar archivos en un formulario, `DropZone`.
- Valida como `DropZone`: `accept`, `maxSize` (base 1024, `formatSize` para escribirlo de otra forma) y `validate` (puede ser asíncrona). Sin `multiple` llega uno solo, el resto se avisa, y si se suelta otro mientras valida gana el último.
- Lo que toma algo de adentro es de eso: una `DropTarget` anidada, un `DropZone` o un `<input type="file">` nativo reciben su archivo y la de afuera solo se apaga. Con un `DropZone scope="window"` en la página, un soltar sobre la `DropTarget` es de ella. La marca es el `preventDefault` del soltar, no `stopPropagation`: un soltar propio que haga `preventDefault` también la deja afuera. No guarda archivos ni tiene `name`.
- Los rechazos quedan abajo hasta el próximo arrastre o soltar: no hay botón para cerrarlos ni se van solos con el tiempo.
- Los textos son los de `DropZone` (`labels.dropZone`: `drop`, `added`, `invalidType`, `tooLarge`, `tooMany`, `locale`). Solo por subpath (`sebs7n-ui/drop-target`).

## Relacionados

[drop-zone](/docs/components/drop-zone.md) · [card](/docs/components/card.md)
