# ContextMenu

A menu that opens at the pointer on right click, on the keyboard context menu keys, or on a touch long press over an area, with the same items, groups, checkbox and radio items, shortcuts and destructive style as DropdownMenu. Every action in it must also be reachable another way.

## Usage

### When to use

- Offer the same actions as a visible menu when a user right-clicks, uses a menu key or long-presses an item.

### When not to use

- The only way to reach an action. Keep the same actions available elsewhere.
- An action menu on touch without a long-press affordance. Use DropdownMenu.

## Examples

### File row

```tsx
<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>
```

### Checkbox options

```tsx
<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuLabel>View</ContextMenuLabel>
      <ContextMenuCheckboxItem checked={menuPinned} onCheckedChange={setMenuPinned}>Pin to top</ContextMenuCheckboxItem>
      <ContextMenuSeparator />
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>
```

### Radio options

```tsx
<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuLabel>Sort by</ContextMenuLabel>
      <ContextMenuRadioGroup value={menuSort} onValueChange={setMenuSort}>
        <ContextMenuRadioItem value="name">Name</ContextMenuRadioItem>
        <ContextMenuRadioItem value="date">Date</ContextMenuRadioItem>
      </ContextMenuRadioGroup>
      <ContextMenuSeparator />
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>
```

### Disabled area

```tsx
<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="File actions" className="flex gap-sm">
    <Button size="sm" variant="outline">Rename</Button>
    <Button size="sm" variant="outline">Delete</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger disabled className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="More actions for report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Right click, long press or Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="File actions">
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Duplicate</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>
```

## Do and don't

### Do

- Mirror a visible menu or toolbar; the context menu is a shortcut.
- Use the same item names as the visible menu.
- Provide an accessible name on ContextMenuContent when the trigger label alone does not explain the actions. ContextMenuTrigger adds tabIndex 0 unless disabled.

### Don't

- Do not make it the only way to an action.
- Do not override the browser menu on text inputs or links.

## Props

| Name | Type | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `onOpenChange` | `(open: boolean) => void` |  |  | Called when it opens or closes. |
| `modal` | `boolean` |  | `true` | Block outside interaction while open. |
| `disabled` | `boolean` |  | `false` | On ContextMenuTrigger: bypasses the custom menu and preserves the browser menu. On item parts: prevents selection. |
| `asChild` | `boolean` |  | `false` | On ContextMenuTrigger or ContextMenuItem: use the only child as the rendered element. Interactive links and text fields keep their native context menu. |
| `longPressDelay` | `number` |  | `500` | Milliseconds a touch must be held before the menu opens. |
| `loop` | `boolean` |  | `false` | On ContextMenuContent: wrap arrow-key navigation from the last enabled item to the first. |
| `avoidCollisions` | `boolean` |  | `true` | On ContextMenuContent: shift or flip the menu to keep it inside the viewport. |
| `collisionPadding` | `number \| Partial<Record<"top" \| "right" \| "bottom" \| "left", number>>` |  | `8` | On ContextMenuContent: minimum distance from viewport edges in px. |
| `onSelect` | `(event: Event) => void` |  |  | On ContextMenuItem: runs when selected by pointer or keyboard. Call event.preventDefault() to keep the menu open. |
| `destructive` | `boolean` |  | `false` | On ContextMenuItem: danger text and icon for a destructive action. |
| `inset` | `boolean` |  | `false` | On ContextMenuItem and ContextMenuLabel: align the text with rows that contain icons or selection indicators. |
| `leadingIcon` | `node` |  |  | On ContextMenuItem: decorative 16px icon. Ignored with asChild. |
| `shortcut` | `string` |  |  | On ContextMenuItem: a displayed key hint; the application handles the shortcut. |
| `checked` | `boolean \| "indeterminate"` |  |  | On ContextMenuCheckboxItem: controlled check state. |
| `onCheckedChange` | `(checked: boolean) => void` |  |  | On ContextMenuCheckboxItem: receives the next check state. |
| `value` | `string` |  |  | On ContextMenuRadioGroup: the selected value. On ContextMenuRadioItem: the required option identifier. |
| `onValueChange` | `(value: string) => void` |  |  | On ContextMenuRadioGroup: receives the selected radio item value. |

### Slots

| Name | Required | Description |
| --- | --- | --- |
| `children` | yes | ContextMenuTrigger and ContextMenuContent with item parts. |

## Anatomy

| Part | Description |
| --- | --- |
| `root` | ContextMenu: holds open state. |
| `trigger` | ContextMenuTrigger: the area that listens for right click. |
| `content` | ContextMenuContent: the same floating list as DropdownMenuContent, positioned at the pointer or trigger. |
| `item` | ContextMenuItem: same as DropdownMenuItem. |
| `icon` | The leading icon of an item, as in DropdownMenu. |
| `destructive` | A destructive ContextMenuItem, as in DropdownMenu. |
| `destructiveIcon` | The leading icon of a destructive item, in the danger text color. |
| `indicator` | The check or radio dot of a checkbox or radio item, as in DropdownMenu. |
| `groupLabel` | ContextMenuLabel: a group heading, as in DropdownMenu. |
| `separator` | ContextMenuSeparator: a line between groups, as in DropdownMenu. |
| `shortcut` | ContextMenuShortcut: the shortcut hint, as in DropdownMenu. |

## Accessibility

Role: `menu`

| Keys | Action |
| --- | --- |
| `Shift+F10 / ContextMenu key` | Opens the menu for the focused trigger area. |
| `ArrowDown / ArrowUp` | Moves between items. |
| `Enter / Space` | Activates the item. |
| `Escape` | Closes the menu and returns focus to the trigger area. |
| `Type a letter` | Moves to the next item starting with it. |

- Items are the DropdownMenu item parts (shared styles and keyboard), exported under ContextMenu names.
- The trigger area must be focusable (tabIndex 0 with an accessible name) for keyboard users.
- The menu is positioned at the pointer, or at the trigger's start when opened from the keyboard.
- Touch: a long press (about 500ms without moving more than a few px) opens the menu at the touch point. The component implements it itself because iOS Safari does not fire contextmenu on long press. If a native contextmenu event follows the long press, it is ignored so the menu does not open twice.
- Mouse and keyboard stay for devices with them attached: right click, Shift+F10 and the Menu key.
- The trigger area sets -webkit-touch-callout: none and user-select: none so the browser callout and text selection do not fight the long press.
- Submenu parts are not exported. Use groups and separators for sections within this menu.

## Install

```bash
npx werewolf-ui add context-menu
```

Run `npx werewolf-ui init` once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See [Installation](https://werewolf-ui.vercel.app/docs/installation.md) for requirements and the alternative registry setup.
