# Context Menu

> Right-click menu anchored at the pointer. Portal-mounted, viewport-clamped, keyboard-navigable. Same item shape as Dropdown Menu.

- Package: `@sisyphos-ui/context-menu`
- Docs: https://sisyphosui.com/docs/components/context-menu
- npm: https://www.npmjs.com/package/@sisyphos-ui/context-menu
- WAI-ARIA pattern: [Menu](https://www.w3.org/WAI/ARIA/apg/patterns/menu/)

## Installation

```bash
pnpm add @sisyphos-ui/context-menu @sisyphos-ui/core
```

Or use the umbrella package:

```bash
pnpm add @sisyphos-ui/ui
```

## Import

```tsx
import "@sisyphos-ui/context-menu/styles.css";
import { ContextMenu } from "@sisyphos-ui/context-menu";
```

## Examples

### Default

Right-click the area to open the menu at the pointer. Portal-mounted + viewport-clamped so it never opens off-screen.

```tsx
import { ContextMenu, toast } from "@sisyphos-ui/ui";

const items = [
  { label: "Edit", onSelect: () => toast.success("Edit") },
  { label: "Duplicate", onSelect: () => toast.success("Duplicated") },
  { type: "separator" as const },
  { label: "Copy link", onSelect: () => toast.info("Link copied") },
  { label: "Move to…", onSelect: () => toast.info("Move") },
  { type: "separator" as const },
  { label: "Delete", destructive: true, onSelect: () => toast.error("Deleted") },
];

export function Example() {
  return (
    <ContextMenu items={items}>
      <div className="flex h-40 items-center justify-center rounded-xl border-2 border-dashed">
        Right-click anywhere in this area
      </div>
    </ContextMenu>
  );
}
```

### Labeled sections

`type: "label"` renders a non-interactive section header. Perfect for grouping destructive actions.

```tsx
import { ContextMenu } from "@sisyphos-ui/ui";

export function Example() {
  return (
    <ContextMenu
      items={[
        { type: "label", label: "Actions" },
        { label: "Rename", onSelect: () => {} },
        { label: "Move", onSelect: () => {} },
        { type: "separator" },
        { type: "label", label: "Danger zone" },
        { label: "Archive", onSelect: () => {} },
        { label: "Delete", destructive: true, onSelect: () => {} },
      ]}
    >
      <div>Project · Q2 launch.md</div>
    </ContextMenu>
  );
}
```

### Per-row table menu

Wrap each `<tr>` in its own `<ContextMenu>` so each row gets a menu scoped to its data.

```tsx
import { ContextMenu } from "@sisyphos-ui/ui";

const rows = [
  { id: 1, name: "Ada Lovelace", role: "Admin" },
  { id: 2, name: "Alan Turing", role: "Member" },
];

export function Example() {
  return (
    <table>
      <tbody>
        {rows.map((row) => (
          <ContextMenu
            key={row.id}
            items={[
              { label: "View profile", onSelect: () => {} },
              { label: "Send message", onSelect: () => {} },
              { type: "separator" },
              { label: "Remove", destructive: true, onSelect: () => {} },
            ]}
          >
            <tr>
              <td>{row.name}</td>
              <td>{row.role}</td>
            </tr>
          </ContextMenu>
        ))}
      </tbody>
    </table>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| **items** (required) | `ContextMenuItem[]` | — | Actions, separators, and labels rendered inside the menu. |
| **children** (required) | `ReactElement` | — | The trigger — any element that accepts `onContextMenu`. |
| margin | `number` | `8` | Viewport margin so the menu never sits flush against an edge. |
| disabled | `boolean` | `false` | When true, right-click is a no-op. |
| emptyState | `ReactNode` | — | Rendered when `items` is empty. |
| onOpenChange | `(open: boolean) => void` | — | Fires when the menu opens or closes. |

## Keyboard interactions

- **ArrowDown + ArrowUp** — Move between items.
- **Home + End** — Jump to first / last item.
- **Enter + Space** — Activate the focused item.
- **Esc** — Close the menu; focus returns to the trigger.
- **Tab** — Close the menu.
