[@layout]: ../docs.layout.md

# Interactions

An interaction is what one part of a page lets someone do: its state and its actions,
defined once for people and agents.

Define it in a component's browser code and bind the visible controls to its actions.
People, Player shortcuts and browser agents ([WebMCP](./webmcp.reference.md)) then use
the same operations, with the same validation and availability.

---

## Define an interaction

This component client keeps a count. It defines a `counter` interaction over its
root element, reports the count as its context, and offers two actions.

```ts
import type { ComponentClientContext } from "@pathmx/core"
import { defineInteraction } from "@pathmx/core/interactions"
import { z } from "zod"

export default function mount({ element }: ComponentClientContext<undefined>) {
  const output = element.querySelector("output")!
  let count = 0
  const show = () => {
    output.value = String(count)
    counter.refresh()
  }
  const counter = defineInteraction(element, {
    kind: "counter",
    read: () => ({ count }),
    actions: {
      increment: {
        title: "Add one",
        state: () => ({ available: true, pending: false }),
        invoke: () => {
          count++
          show()
        },
      },
      set: {
        title: "Set the count",
        description: "Replace the count with a whole number.",
        input: z.object({ value: z.number().int() }),
        state: () => ({ available: true, pending: false }),
        invoke: ({ value }) => {
          count = value
          show()
        },
      },
    },
  })
  counter.bind(element.querySelector("button")!, { action: "increment" })
  return { dispose: () => counter.dispose() }
}
```

- `kind` names what the interaction is; `read()` returns the context a reader sees.
- Each action has a `title`, an optional `description`, an optional Zod `input`, a
  `state()` with `available`, `pending` and an optional `reason`, and `invoke()`.
  Invocation validates input against the schema and refuses an unavailable action.
- Call `refresh()` whenever the context or an action's state changes, so discovery
  and bound controls update.
- `dispose()` removes the interaction and its bindings.

---

## Bind controls

`bind(element, { action })` declares which control performs an action. A bound
`<button>` invokes its action when clicked, so don't add a click listener of your
own: the action would run twice. A submit button inside a form keeps the form's
native submission.

| Option | Meaning |
|---|---|
| `action` | The action the control performs. |
| `input` | For an action with input: a function returning `{ value }` from the control, or `undefined` while it isn't ready. |
| `scope` | `"local"` (default) matches the control itself; `"interaction"` also matches anything inside the interaction's members. |
| `present` | Returns `{ visible, label }` when the control's own text isn't its label. |

Pass `members: () => elements` in the definition when the interaction spans elements
outside its root, such as fields an editor makes editable elsewhere on the page.

---

## Discover and invoke

`interactionRegistry` lists the interactions within some part of the document.
Each result has the interaction's `surface` (its `kind`, `context`, and each
action's title, description, input as JSON Schema and state), a `handle` to read and
invoke it, and its `bindings`.

```ts
import { interactionRegistry } from "@pathmx/core/interactions"

const [counter] = interactionRegistry
  .discover({ within: [document.body] })
  .filter(({ surface }) => surface.kind === "counter")
counter?.surface.context // { count: 0 }
await counter?.handle.invoke("set", { value: 3 })

const stop = interactionRegistry.subscribe({ within: [document.body] }, (found) => {
  // Called when interactions appear, disappear or change.
})
```

An invocation that can't run rejects with an `InteractionInvocationError` whose
`code` is `unavailable`, `busy`, `invalid-input` (with the schema's `issues`),
`disposed` or `aborted`.
