## context_menu (`poetry_context_menu`)

A menu of actions revealed by right-clicking an element.

Class: Poetry::Ui::ContextMenu::Component - BEM block `poetry-ui-context_menu`.
Slot REQUIRED: with_trigger (the right-click surface) - a call without it raises.
Slot REQUIRED: with_item (at least one item) - a call without it raises.
- `dir:` (symbol) - one of ltr|rtl - Writing-direction override (ltr/rtl) stamped on the root.
- `disabled:` (boolean) - default false - Inerts the surface - no gesture opens the menu.
- `focusable_surface:` (boolean) - default false - Puts the surface in the tab order and advertises Shift+F10.
- `label:` (string) - The menu's accessible name (localized fallback when omitted).
- `long_press_delay:` (integer) - default 700 - Touch long-press duration in ms before the menu opens.
- `loop:` (boolean) - default false - Wraps arrow-key movement past either end of the menu.
- `modal:` (boolean) - default true - Traps focus in the open menu; false keeps the page interactive.
- `open:` (boolean) - default false - Server-renders the menu open (rare - context menus normally open from the gesture).
- `side:` (symbol) - one of top|right|bottom|left, default "right" - Which side of the pointer the menu opens toward; collisions may still flip it.
Slots: items (The menu composition API: one ordered items collection accepting seven kinds, interleaved in call order - with_item an action row (href: renders it as a real link; submit: as a real submit button) with_checkbox_item a toggleable checked/unchecked row with_radio_group a single-select scope; add rows inside it via with_radio_item(value:) with_label a non-interactive heading for a run of items with_separator a horizontal rule between runs with_group semantic grouping around the same union, one level down with_sub a nested submenu: its own with_trigger plus the same union, recursively; many; types item|checkbox_item|radio_group|label|separator|group|sub - one with_<type> setter each, options as keywords; with_item/with_checkbox_item/with_label yield NOTHING to the block - no |param|, write content directly; each with_radio_group REQUIRES with_radio_item inside its block (at least one radio item); each with_group REQUIRES with_item inside its block (at least one item); each with_sub REQUIRES with_trigger inside its block (the sub-menu item); each with_sub REQUIRES with_item inside its block (at least one item)), trigger (The right-click/long-press SURFACE: wraps arbitrary content (a card, a row, a region); polymorphic tag: (default :span, set tag: :div to wrap block content). NOT a button: no role, no aria-haspopup, no tabindex by default. The inline -webkit-touch-callout suppresses the iOS callout so long-press can run (iOS never fires contextmenu; the timer is the only touch path there).; with_trigger yields NOTHING to the block - no |param|, write content directly).
- PART `context-menu` - Root wrapper hosting the context-menu + menu + popper controllers around the surface and content
- PART `context-menu-trigger` - The right-click/long-press SURFACE wrapping the logical object - not a widget: no role, no aria-haspopup | states: data-popup-open (the menu is open (absence is the closed state - no aria-expanded on a role-less surface)); data-disabled (the surface is inert (disabled: true))
- PART `context-menu-content` - The role=menu popup panel - anchored at the pointer via popper's virtual-anchor mode; open state and animation ride here | states: data-open (menu is open (presence flips the pair at runtime)); data-closed (menu is closed or animating out (the server-rendered state)); data-side=top|right|bottom|left (the placement side (the side: option, default right; popper re-writes it after collision flips)); data-align=start|center|end (the alignment (forced start initially; popper re-resolves it)) | vars: --transform-origin (popper's anchor-facing animation origin); --available-width (popper: viewport space left for the panel (post-flip)); --available-height (popper: viewport space left for the panel (post-flip)); --anchor-width (popper: the anchor rect's measured width); --anchor-height (popper: the anchor rect's measured height)
- PART `context-menu-group` - role=group semantic grouping between separators
- PART `context-menu-label` - Non-interactive heading for a run of items | states: data-inset (indented to align with checkbox/radio item text (inset: true))
- PART `context-menu-item` - One role=menuitem action row | states: data-variant (default or destructive (the danger treatment)); data-inset (indented to align with checkbox/radio item text (inset: true)); data-disabled (item is disabled (always written together with aria-disabled))
- PART `context-menu-checkbox-item` - A role=menuitemcheckbox toggle row | states: data-checked (checked (the controller re-writes the pair with aria-checked on activation)); data-unchecked (unchecked); data-disabled (item is disabled (always written together with aria-disabled)); data-close-on-select (per-item override of the menu's close-on-select default ("false" keeps the menu open))
- PART `context-menu-radio-group` - role=group scoping one single-select value | states: data-value (the selected radio value (the controller re-writes it on change))
- PART `context-menu-radio-item` - A role=menuitemradio row inside a radio group | states: data-checked (the selected radio (the controller re-writes the pair with aria-checked)); data-unchecked (not selected); data-value (the radio's value); data-disabled (item is disabled (always written together with aria-disabled))
- PART `context-menu-checkbox-item-indicator` - The check glyph inside checkbox items (aria-hidden; the item carries the checked state)
- PART `context-menu-radio-item-indicator` - The circle glyph inside radio items (aria-hidden; the item carries the checked state)
- PART `context-menu-separator` - role=separator rule between groups
- PART `context-menu-shortcut` - The trailing keybinding HINT - aria-hidden, never binds the key
- PART `context-menu-sub` - A submenu scope - hosts its own popper around the sub trigger/content pair
- PART `context-menu-sub-trigger` - The role=menuitem row opening its submenu | states: data-popup-open (its submenu is open (written with aria-expanded; absence is the closed state)); data-inset (indented to align with checkbox/radio item text (inset: true))
- PART `context-menu-sub-content` - The nested role=menu panel - its own popper content on the same presence machinery | states: data-open (submenu is open (presence flips the pair at runtime)); data-closed (submenu is closed (the server-rendered state)); data-side=top|right|bottom|left (the placement side (right/left by direction; popper resolves it at runtime)); data-align=start|center|end (the alignment against the sub-trigger (popper resolves it at runtime)) | vars: --transform-origin (popper's anchor-facing animation origin); --available-width (popper: viewport space left for the panel (post-flip)); --available-height (popper: viewport space left for the panel (post-flip)); --anchor-width (popper: the sub-trigger's measured width); --anchor-height (popper: the sub-trigger's measured height)
- WIRING root: `poetry--core--context-menu` registers; values long_press_delay, disabled | `poetry--core--menu` registers; values open, modal, loop | `poetry--core--popper` registers; values side, align, side_offset, avoid_collisions
- WIRING trigger: `poetry--core--context-menu` actions open on contextmenu, pressStart on pointerdown, pressCancel on pointermove/pointerup/pointercancel | `poetry--core--popper` targets anchor
- WIRING content: `poetry--core--popper` targets content
- WIRING item: `poetry--core--menu` actions activate on click
- WIRING sub_trigger: `poetry--core--menu` actions subEnter on pointerenter, subLeave on pointerleave, openSub on click | `poetry--core--popper` targets anchor
- RULE: NEVER make a context menu the only path to an action - it is an invisible affordance; every item needs a visible equivalent (a '...' DropdownMenu button, a toolbar, a detail page).
- RULE: Choose ContextMenu only for right-click-on-an-object semantics; a visible button opening a menu is DropdownMenu.
- RULE: Do not add aria-haspopup or a role to the trigger surface; do not make it focusable except via focusable_surface: true.
- RULE: side: picks which side of the pointer the menu opens toward (top/right/bottom/left, default :right); align and offsets are not API - collisions still flip the side.
- RULE: Wrap the whole logical object (row/card) as the trigger surface, not a fragment.
- RULE: Destructive items use variant: :destructive AND still confirm irreversible actions via a dialog.
- RULE: shortcut: is a visual hint only - it does NOT bind the key.
- RULE: Do not nest a ContextMenu trigger surface inside another ContextMenu trigger surface.
