<!-- 04_VISUAL/xwui/xwui-lib/xwui-lib-basic/src/action/XWUISwipeRow/XWUISwipeRow.md -->

# XWUISwipeRow

A list row whose content slides sideways to reveal action buttons. The content is a host-owned slot the page fills; the
component owns the actions, the gesture, the keyboard path and the announcements.

```ts
config: { actions?: { start?: Action[]; end?: Action[] }; threshold?: 40; snapBack?: true; disabled?: false;
          actionWidth?: 76; mouseDrag?: false; tapClose?: true; divider?: true; className?;
          labels?: { region?; start?; end?; opened?; closed? } }
Action: { id; label; icon?; tone?: accent|neutral|success|warning|danger; ariaLabel? }
data:   { open?: 'start' | 'end' | null }
events: logic open {side, source} | close {side, source} | action {id, side}
        host DOM (bubbling) 'swipe-row-open' | 'swipe-row-close' | 'swipe-row-action'
methods: onAction(fn) -> off, open(side), close(), isOpen(), mountContent((host, factory) => child) -> cleanup,
         getContentHost(), focus(), updateConfig()
```

`start` is the inline-start edge: left in LTR, right in RTL. A start action is revealed by dragging toward the inline end,
an end action toward the inline start. RTL mirrors with no extra config. Setting `data.open` from the page raises no
event (the page authored that change); drag, keys, focus and the buttons raise events with their `source`.

## Gesture

- Touch and pen drag the row; `touch-action: pan-y` keeps vertical scrolling. A move past 6 px decides: vertical wins and
  the row is never touched, horizontal claims the drag with pointer capture.
- Release past `threshold` px, or a quick flick that also travelled a little, moves ONE step in the dragged direction
  (closed to open, open to closed). A row never jumps from one side to the other in one gesture.
- A short slow drag snaps back. `pointercancel` (the browser took it for a scroll) puts the row back.
- **Mouse is optional**: off by default (`mouseDrag: false`) because a mouse drag fights text selection and link
  dragging; with it on, the native link drag is refused while a press is active. A mouse user has the keyboard path.
- The click that ends a drag never reaches the content. With `tapClose` a pointer click on the content of an open row
  closes it instead of reaching the content; a keyboard click (Enter on a link) always passes.

## Keyboard and screen reader

- Every action is a real `<button>` always in the tree: a closed strip is only zero wide. Tab into one and its side
  opens (visible on focus); focus leaving the row closes a row opened that way.
- Shift+ArrowLeft / Shift+ArrowRight (physical, mirrored in RTL): reveal a side, or close the open one. Focus moves to
  the first action, or back to the content on close. Escape closes. Announced by `aria-keyshortcuts`.
- Pressing an action emits `action`, then `snapBack` closes the row and returns focus to the content when it was in the
  strip. Strips are named groups when `labels.start` / `labels.end` are given; `labels.opened` / `labels.closed` are
  spoken by a polite status line after a user opens or closes the row. The component has no copy of its own.
- `prefers-reduced-motion` removes the slide animation (the strip transition). Tab order: content, start strip, end strip.

## Performance

A drag writes ONE unitless custom property (`--xwui-swipe-row-offset`) on the root per animation frame (rAF with a timer
fallback); CSS sizes the strips from it. No re-render, no layout read. Only decisions go through state.

## Verify

```bash
node xwui-lib-super/scripts/test-component.mjs SwipeRow
```

Tester: `testers/TesterXWUISwipeRow.html`; supports theme and direction query fields.
