# Menu

A popover of actions or choices beside the control that opened it. Open one from anywhere with showMenu.

![A popover beside its trigger with a small title; a dangerous item is red and confirms in a Dialog once the menu is gone.](https://ui.terratcg.app/previews/overlay/menu-actions/ios-light.png)

_iOS and Android only: no web preview._

```tsx
import Flag02Icon from "@hugeicons-pro/core-bulk-rounded/Flag02Icon";
import MoreHorizontalIcon from "@hugeicons-pro/core-bulk-rounded/MoreHorizontalIcon";
import Share08Icon from "@hugeicons-pro/core-bulk-rounded/Share08Icon";
import UserBlock01Icon from "@hugeicons-pro/core-bulk-rounded/UserBlock01Icon";
import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react-native";
import { Button, ButtonIcon, showDialog, showMenu, type ButtonIconProps, type OverlayTrigger } from "@terratcg/ui";
import { useRef, type RefObject } from "react";
import type { ViewInstance } from "react-native";

function icon(svg: IconSvgElement): ButtonIconProps["children"] {
  return function SlotIcon({ color, size }) {
    return <HugeiconsIcon icon={svg} color={color} size={size} />;
  };
}

function openPersonMenu(trigger: RefObject<OverlayTrigger | null>) {
  void showMenu(trigger, {
    title: "camille_r",
    items: [
      { label: "Share profile", icon: icon(Share08Icon), onSelect: () => {} },
      { label: "Report", icon: icon(Flag02Icon), onSelect: () => {} },
      {
        label: "Block",
        icon: icon(UserBlock01Icon),
        destructive: true,
        onSelect: ({ trigger: from }) =>
          void showDialog({
            tone: "destructive",
            title: "Block camille_r?",
            body: "They can't message you or see your library.",
            cancel: "Cancel",
            confirm: "Block",
            icon: icon(UserBlock01Icon),
            trigger: from,
          }),
      },
    ],
  });
}

export default function MenuActions({ capture }: SceneProps) {
  const trigger = useRef<ViewInstance>(null);
  return (
    <Button
      ref={trigger}
      variant="ghost"
      size="icon"
      accessibilityLabel="More"
      // The capture opens it once the trigger is laid out, so it has a place to open from.
      onLayout={capture ? () => openPersonMenu(trigger) : undefined}
      onPress={() => openPersonMenu(trigger)}
    >
      <ButtonIcon>{icon(MoreHorizontalIcon)}</ButtonIcon>
    </Button>
  );
}
```

## Import

```tsx
import { showMenu } from "@terratcg/ui";
```

## Usage

```tsx
const trigger = useRef<ViewInstance>(null);
const openMenu = () =>
  showMenu(trigger, {
    title: "camille_r",
    items: [
      { label: "Share profile", onSelect: share },
      { label: "Block", destructive: true, onSelect: ({ trigger }) => confirmBlock(trigger) },
    ],
  });

<Button ref={trigger} variant="ghost" size="icon" accessibilityLabel="More" onPress={openMenu}>
  <ButtonIcon>{moreIcon}</ButtonIcon>
</Button>;
```

Menus appear in the `OverlayHost`, which you mount once at the root inside `ThemeProvider`. `showMenu` measures `trigger` and opens beside it, aligned to its trailing edge: below it, or above when there is no room. It closes on a pick, an outside tap or Back, and runs the picked item's `onSelect` only once it has left the screen, so `onSelect` may navigate or open a Dialog. Inside a native sheet, wrap the sheet's content in `SheetOrigin` so the menu opens in the sheet.

The plate stays on screen. A small title names what the menu acts on, and the items' text lines up with it. A `destructive` item is drawn in the danger colour, and the current choice of a value menu wears an accent dot and a bold label. There is no Cancel item: an outside tap or Back closes the menu.

## Examples

### Picking a value

Mark the current choice selected: it takes the dot and a bold label.

![Mark the current choice selected: it takes the dot and a bold label.](https://ui.terratcg.app/previews/overlay/menu-picker/ios-light.png)

_iOS and Android only: no web preview._

```tsx
import Sorting05Icon from "@hugeicons-pro/core-bulk-rounded/Sorting05Icon";
import { HugeiconsIcon } from "@hugeicons/react-native";
import { Button, ButtonIcon, ButtonText, showMenu } from "@terratcg/ui";
import { useRef, useState } from "react";
import type { ViewInstance } from "react-native";

const SORTS = ["Newest", "Size", "Name"] as const;

export default function MenuPicker({ capture }: SceneProps) {
  const trigger = useRef<ViewInstance>(null);
  const [sort, setSort] = useState<(typeof SORTS)[number]>("Newest");
  const open = () =>
    void showMenu(trigger, {
      title: "Sort by",
      items: SORTS.map((label) => ({ label, selected: label === sort, onSelect: () => setSort(label) })),
    });
  return (
    <Button ref={trigger} variant="outline" onLayout={capture ? open : undefined} onPress={open}>
      <ButtonIcon>{({ color, size }) => <HugeiconsIcon icon={Sorting05Icon} color={color} size={size} />}</ButtonIcon>
      <ButtonText>{sort}</ButtonText>
    </Button>
  );
}
```

## Props

### showMenu

### MenuSpec

| Prop    | Type                  | Description                                                     |
| ------- | --------------------- | --------------------------------------------------------------- |
| `title` | `string`              | What the menu is about: a person's name, "Sort by".             |
| `items` | `readonly MenuItem[]` | The actions or values, in order. Put a destructive action last. |

### MenuItem

### MenuItem

| Prop           | Type                                                             | Description                                                                                                                                              |
| -------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label`        | `string`                                                         | What the item does, or the value it picks.                                                                                                               |
| `icon?`        | `((slot: ButtonIconSlot) => React.ReactNode) \| React.ReactNode` | Leading icon, in the item's colour. A Dialog opened from the item repeats it on its confirm key.                                                         |
| `destructive?` | `boolean`                                                        | Red. Confirm it with a Dialog from `onSelect`.                                                                                                           |
| `selected?`    | `boolean`                                                        | The current choice of a menu that picks a value.                                                                                                         |
| `onSelect`     | `(pick: MenuPick) => void`                                       | Runs once the menu has left the screen, so it may navigate or open a Dialog. A Dialog it opens takes `trigger` to hand focus back to the Menu's trigger. |

### MenuPick

What an item's `onSelect` receives.

### MenuPick

What a picked item's `onSelect` receives.

| Prop      | Type                                | Description                                                                   |
| --------- | ----------------------------------- | ----------------------------------------------------------------------------- |
| `trigger` | `RefObject<OverlayTrigger \| null>` | The Menu's trigger: pass it as a Dialog's `trigger` so focus goes back there. |

## Accessibility

- **Roles:** the plate is a `menu`, each item a `menuitem`, and the current choice has the `selected` state. Item icons are hidden from screen readers.
- **Focus:** once the menu is placed, screen-reader focus moves to its title, which has the `header` role. When it closes, focus goes back to the trigger before `onSelect` runs.
- **Modal:** the menu is a modal layer; the escape gesture closes it.
- **Larger text:** labels wrap instead of truncating, and the menu flips above its trigger when the taller plate doesn't fit below.
- **Reduce Motion:** the plate fades in without scaling.

## Platform differences

- **iOS:** the menu sits in a full-window overlay, above native sheets.
- **Android:** the menu is a transparent `Modal`, so Back closes it, and it is placed from the trigger's page coordinates.
- **Feedback:** a pick calls ThemeProvider's `feedback` handler with `"press"`. The keyboard is dismissed before the menu opens.
- **Queue:** a Menu waits behind a Dialog that is up, and a second `showMenu` while one is open does nothing.
- **No web tab:** a live Menu shares the Dialog's overlay layer, which on a desktop browser covers the whole page because nothing caps its width yet. Until it gets a width cap (#702), this page shows only the iOS and Android captures.

## Rules

- Title the menu with what it is about: a person's name, "Sort by".
- Keep items to short verbs or values. A menu that picks a value marks the current one `selected`.
- Put a destructive item last, and confirm it with a Dialog from `onSelect`, passing on its `trigger`.
- Give an icon to every item or to none.
- Open a menu from a control the user pressed, never on its own.
