# ContextMenu

> El menú del botón derecho: las mismas acciones del `DropdownMenu`, ancladas al puntero.

```tsx
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, … } from "sebs7n-ui/context-menu"
```

## Ejemplos

### Archivos de una biblioteca

Como la lista de iCloud Drive: el mismo menú desde el click derecho sobre la fila y desde el botón «…» de la derecha, que es el que se ve. Con foco en la fila, la tecla de menú contextual o Shift+F10 también lo abren. «Eliminar» va al final, en rojo.

```tsx
import { Button } from "sebs7n-ui/button"
import { ContextMenu, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from "sebs7n-ui/context-menu"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "sebs7n-ui/dropdown-menu"
import { Fragment } from "react"
import { MoreHorizontalIcon } from "lucide-react"

function Archivos() {
  return (
    <ul aria-label="Archivos" className="flex w-full max-w-xl flex-col">
      {ARCHIVOS.map((archivo) => (
        <li key={archivo.nombre} className="relative">
          <ContextMenu>
            <ContextMenuTrigger className="flex h-10 w-full items-center gap-3 rounded-item pr-12 pl-3 text-left hover:bg-fill-1">
              <span aria-hidden="true" className="flex size-6 shrink-0 items-center justify-center [&_svg]:size-5">
                {archivo.icon}
              </span>
              <span className="min-w-0 flex-1 truncate text-body text-label">{archivo.nombre}</span>
              <span className="hidden w-32 text-callout text-label-secondary sm:block">{archivo.tipo}</span>
              <span className="w-14 text-right text-callout text-label-secondary">{archivo.peso}</span>
            </ContextMenuTrigger>
            <ContextMenuContent>
              {ACCIONES.map((grupo, i) => (
                <Fragment key={grupo[0]!.label}>
                  {i > 0 && <ContextMenuSeparator />}
                  <ContextMenuGroup>
                    {grupo.map((accion) => (
                      <ContextMenuItem key={accion.label} variant={accion.destructive ? "destructive" : "default"}>
                        {accion.icon}
                        {accion.label}
                      </ContextMenuItem>
                    ))}
                  </ContextMenuGroup>
                </Fragment>
              ))}
            </ContextMenuContent>
          </ContextMenu>

          <DropdownMenu>
            <DropdownMenuTrigger
              render={<Button aria-label={`Acciones de ${archivo.nombre}`} size="icon-sm" variant="plain" />}
              className="absolute top-1.5 right-1.5"
            >
              <MoreHorizontalIcon />
            </DropdownMenuTrigger>
            {/* Como en Drive: el menú se abre al costado del botón, alineado con la fila. */}
            <DropdownMenuContent align="start" side="left">
              {ACCIONES.map((grupo, i) => (
                <Fragment key={grupo[0]!.label}>
                  {i > 0 && <DropdownMenuSeparator />}
                  {grupo.map((accion) => (
                    <DropdownMenuItem key={accion.label} variant={accion.destructive ? "destructive" : "default"}>
                      {accion.icon}
                      {accion.label}
                    </DropdownMenuItem>
                  ))}
                </Fragment>
              ))}
            </DropdownMenuContent>
          </DropdownMenu>
        </li>
      ))}
    </ul>
  )
}
```

### El lienzo de un editor

Checks y radios que no cierran el menú, para probar varias opciones sin volver a abrirlo. Acá el click derecho gana: sobre un lienzo no hay dónde poner un botón que no tape el trabajo.

```tsx
import { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuTrigger } from "sebs7n-ui/context-menu"
import { CopyIcon } from "lucide-react"
import { useState } from "react"

function Lienzo() {
  const [zoom, setZoom] = useState("100")
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex h-40 w-full max-w-md items-center justify-center rounded-control border border-dashed border-separator bg-fill-1 text-callout text-label-secondary">
        Click derecho sobre el lienzo
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuGroup>
          <ContextMenuLabel>Mostrar</ContextMenuLabel>
          <ContextMenuCheckboxItem defaultChecked>Grilla</ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem defaultChecked>Guías</ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem>Reglas</ContextMenuCheckboxItem>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuGroup>
          <ContextMenuLabel>Zoom</ContextMenuLabel>
          <ContextMenuRadioGroup onValueChange={setZoom} value={zoom}>
            <ContextMenuRadioItem value="50">50 %</ContextMenuRadioItem>
            <ContextMenuRadioItem value="100">100 %</ContextMenuRadioItem>
            <ContextMenuRadioItem value="200">200 %</ContextMenuRadioItem>
          </ContextMenuRadioGroup>
        </ContextMenuGroup>
        <ContextMenuSeparator />
        <ContextMenuItem>
          <CopyIcon />
          Duplicar selección
          <ContextMenuShortcut>⌘D</ContextMenuShortcut>
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}
```

## Props

### ContextMenu

Hereda las props de `ContextMenu.Root`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `actionsRef` | `React.RefObject<MenuRoot.Actions \| null>` | — | **Heredada de Base UI.** Ref con las acciones imperativas de Base UI (`unmount()`), para desmontarlo sin esperar la animación de salida. |
| `defaultOpen` | `boolean` | — | **Heredada de Base UI.** Si arranca abierto. Es la versión no controlada de `open`. |
| `disabled` | `boolean` | — | **Heredada de Base UI.** Apaga la interacción y lo marca con `data-disabled`, que es el atributo del que cuelgan los estilos de apagado. |
| `loopFocus` | `boolean` | — | **Heredada de Base UI.** Si al pasar del último elemento el foco vuelve al primero. |
| `onOpenChange` | `((open: boolean, eventDetails: ContextMenuRoot.ChangeEventDetails) => void)` | — | **Heredada de Base UI.** Se llama con el estado nuevo cada vez que se abre o se cierra. |
| `open` | `boolean` | — | **Heredada de Base UI.** Si está abierto. Pasarla lo vuelve controlado: sin `onOpenChange` ya no se cierra solo. |
| `orientation` | `"horizontal" \| "vertical"` | — | **Heredada de Base UI.** `horizontal` (default) o `vertical`. Define qué flechas mueven el foco. |

### ContextMenuCheckboxItem

Hereda las props de `ContextMenu.CheckboxItem`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `inset` | `boolean` | — | Corre el texto a la canaleta del tilde (`pl-7`) para que alinee con los `CheckboxItem` y `RadioItem` del mismo menú. En un menú sin tildes no hace falta. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuContent

Hereda las props de `ContextMenu.Popup` y `ContextMenu.Positioner`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `align` | `"center" \| "end" \| "start"` | — | Cómo se alinea el panel sobre el eje transversal. |
| `alignOffset` | `MenuPositionerProps['alignOffset']` | — | Corrimiento en píxeles sobre el eje de alineación. |
| `side` | `"left" \| "right" \| "top" \| "bottom" \| "inline-end" \| "inline-start"` | — | De qué lado del ancla se abre el panel. |
| `sideOffset` | `MenuPositionerProps['sideOffset']` | — | Distancia en píxeles entre el ancla y el panel. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuGroup

Hereda las props de `ContextMenu.Group`.

Sin props propias: pasa todo al primitivo.

### ContextMenuItem

Hereda las props de `ContextMenu.Item`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `external` | `boolean` | — | Lleva a otro sitio: texto en el acento y ↗ al final. |
| `inset` | `boolean` | — | Alinea el texto con el de los ítems que llevan ícono. |
| `variant` | `"default" \| "destructive"` | `"default"` | `destructive`: texto e ícono en rojo, para la acción que borra. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuLabel

Hereda las props de `ContextMenu.GroupLabel`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `inset` | `boolean` | — | Corre el texto a la canaleta del tilde (`pl-7`) para que alinee con los `CheckboxItem` y `RadioItem` del mismo menú. En un menú sin tildes no hace falta. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuRadioGroup

Hereda las props de `ContextMenu.RadioGroup`.

Sin props propias: pasa todo al primitivo.

### ContextMenuRadioItem

Hereda las props de `ContextMenu.RadioItem`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `inset` | `boolean` | — | Corre el texto a la canaleta del tilde (`pl-7`) para que alinee con los `CheckboxItem` y `RadioItem` del mismo menú. En un menú sin tildes no hace falta. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuSeparator

Hereda las props de `ContextMenu.Separator`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuShortcut

Hereda las props de `<span>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuSub

Hereda las props de `ContextMenu.SubmenuRoot`.

Sin props propias: pasa todo al primitivo.

### ContextMenuSubContent

Hereda las props de `ContextMenu.Popup` y `ContextMenu.Positioner`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `align` | `"center" \| "end" \| "start"` | `"start"` | Cómo se alinea el panel sobre el eje transversal. |
| `alignOffset` | `MenuPositionerProps['alignOffset']` | `-5` | Corrimiento en píxeles sobre el eje de alineación. |
| `sideOffset` | `MenuPositionerProps['sideOffset']` | `-5` | Distancia en píxeles entre el ancla y el panel. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuSubTrigger

Hereda las props de `ContextMenu.SubmenuTrigger`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `inset` | `boolean` | — | Corre el texto a la canaleta del tilde (`pl-7`) para que alinee con los `CheckboxItem` y `RadioItem` del mismo menú. En un menú sin tildes no hace falta. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

### ContextMenuTrigger

Hereda las props de `ContextMenu.Trigger`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `focusable` | `boolean` | `true` | `false` saca la parada de tabulación y con ella la apertura por teclado. |
| `onKeyDown` | `KeyboardEventHandler<T>` | — | Corre **antes** que el manejador propio del trigger. Si hacés `preventDefault()`, la apertura por Shift+F10 no llega a dispararse. |
| `tabIndex` | `number` | — | Pisa el `0` que pone `focusable`. Casi nunca hace falta: es la salida para meter el área en un orden de tabulación armado a mano. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

## Teclado

| Tecla | Qué hace |
|---|---|
| Tab | Llega al área disparadora, que es una sola parada de tabulación. |
| Menú contextual · Shift+F10 | Abre con el foco en el área, anclado a ella y no al puntero. |
| Click derecho | Abre en el punto exacto del puntero. |
| Mantener apretado (touch) | Abre a los 500 ms; moverse más de 10 px cancela. |
| ↑ ↓ | Recorre los ítems. |
| → ← | Entra y sale de un submenú. |
| Escribir | Salta al ítem que empieza con esas letras. |
| Enter · Espacio | Ejecuta y cierra. |
| Escape | Cierra y devuelve el foco al área. |

## Accesibilidad

- El trigger es un `<div>`: sin `tabIndex` no le llega el foco y la tecla de menú contextual no tiene sobre qué disparar. `ContextMenuTrigger` lo hace enfocable y sintetiza el evento `contextmenu`, así que la apertura por teclado funciona aunque Base UI no la traiga.
- Lleva `aria-haspopup="menu"` y `aria-keyshortcuts="Shift+F10"`: sobre un área sin botón visible, el atajo es lo único que se puede anunciar.
- El panel es el mismo `role="menu"` del `DropdownMenu`: atrapa el foco, se recorre con flechas y al cerrar lo devuelve al área.
- `focusable={false}` saca la parada de tabulación **y** la apertura por teclado: solo si el área ya tiene adentro un control enfocable que abre el mismo menú.

## Reglas de uso

- **Nunca puede ser el único camino a una acción.** El click derecho no se ve, no se descubre y en touch depende de un long-press que la mitad de la gente no conoce. Todo lo que esté acá tiene que estar también en un botón visible, en un `DropdownMenu` o en un atajo anunciado.
- **Es un atajo, no una puerta de entrada.** Se pone sobre lo que ya tiene sus acciones a la vista: una fila, una tarjeta, un lienzo.
- Comparte el panel con `DropdownMenu` a propósito: son el mismo menú abierto de dos maneras, no dos componentes. En una lista de archivos, como iCloud Drive, el mismo menú sale del click derecho sobre la fila y del botón «…» de la fila, que es el que se ve.
- `ContextMenuShortcut` pesa más acá que en un `DropdownMenu`: es el cartel que enseña el camino alternativo.
- Sobre un `<input>` o un `<textarea>` no: te comés el menú de corregir, copiar y pegar del navegador.

## Relacionados

[dropdown-menu](/docs/components/dropdown-menu.md) · [menubar](/docs/components/menubar.md) · [toolbar](/docs/components/toolbar.md) · [popover](/docs/components/popover.md)
