# `Input.drag`

> Contract 2.0.0 · package sha256 `a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e` · generated from `configurator-runtime.d.ts` by `services/onsite-scripter/scripts/build-contract-docs.mjs`; edit the source, not this file.

Method of [`Input`](Input.md) · stable · since 2.0.0 · called as `session.input.drag`

```ts
drag(query: Query, options: DragOptions): () => void;
```

Makes parts draggable. Lifecycle call: immediate. Each call adds a registration, and a press
uses the newest one whose query matches the part; the returned function or `signal` removes
it. Neither a new registration nor a removal ends a drag in progress, which completes under
the options it started with. During a drag: 'drag:start', 'drag:move', 'drag:end'; no 'click'
follows a drag that moved. A lost pointer capture ends it with reason 'cancelled'. A
transaction requested during a drag opens after 'drag:end'. Returns the unregister function.
Errors: throws HoloInputError on invalid options. A slow `constrain` records 'frame-budget'.

## Options

Through [`Session`](Session.md):

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `commit` | `'runtime' \| 'app'` | 'runtime' | 'runtime': at release the runtime writes the pose to the scene model (one recorded commit, actor 'shopper'), which enters the write queue then, behind any… |
| `constrain` | `(move: DragMove, frame: Frame) => PoseInput \| null` | undefined (the unit follows `move.pose`) | Synchronous. |
| `on` | `'floor' \| Query \| ((part: Part) => 'floor' \| Query)` | — | The surface the proposed pose lies on; a function picks it per part at press time. |
| `signal` | `AbortSignal` | undefined | Aborting removes this registration; a drag in progress completes. |
| `undo` | `CommitOptions['undo']` | 'Move' | Undo label of a runtime commit, as [`CommitOptions.undo`](CommitOptions.undo.md) ('rebase' erases every earlier step). |

## Abort (`signal`)

This call is a creator (`SignalOption.signal`, [the kinds of call](../read-this-first.md#conventions)).

> A creator (`collection` for a new name, `materials.create`, `on`, `onFrame`, `beforeCommit`, `follow`, `input.drag`, `labs.lightLinking`) given a signal already aborted returns at once, already ended: a `Collection` or `Material` reading `disposed: true` that holds no content and no live name, or a registration that never runs and whose disposer does nothing.
>
> It records no Problem for the abort and throws only what it would throw anyway, as the DOM's `addEventListener` with an aborted signal adds nothing (spec section 6.5).

## Example

Imports: values (`has`, `validate`, the `Holo*Error` classes, the pure helpers) from `@holostep-configurators/runtime-client`, types from `@holostep-configurators/runtime-client/contract`, `/math` and `/testing` names from those entries; `boot` from the runtime module the root's `loadRuntimeAtFloor` resolves to. A type your page defines is your app's: declare it there, never import it from the package.

Compiled under the contract's strict settings (`examples/declaration/11-Input.drag.ts`):

```ts
import { has } from '@holostep-configurators/runtime-client';
import type { Pose, Ray, Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
// The app owns placement: the runtime shows the constrained pose, the store decides on release.
declare const planner: {
  snap(pose: Pose, ray: Ray): Pose;
  canDrop(node: string, pose: Pose): boolean;
};
// movePlaced returns false when the store keeps the node where it is (an equal sync)
declare const store: { movePlaced(node: string, pose: Pose): boolean };
declare const editing: AbortSignal;                        // aborts when editing ends
// commit: 'app' and constrain are experimental; this has() also brings input.revert
if (has(session, 'input.drag.commit.app') && session.supports('input.drag.constrain')) {
  session.input.drag({ role: 'product' }, {
    on: 'floor',
    commit: 'app',                                         // the runtime writes nothing at release
    constrain: (move) => {
      const p = planner.snap(move.pose, move.ray);
      // one rotation: a Pose is not a PoseInput
      return { position: p.position, quaternion: p.quaternion, scale: p.scale };
    },
    signal: editing,
  });
  // the page's signal, not editing: a drag in progress completes after the removal
  session.on('drag:end', ({ part, pose, moved, reason }) => {
    if (!moved || !part.node) return;
    if (reason !== 'released' || !planner.canDrop(part.node, pose)) {
      session.input.revert(part);                          // back to the scene model's pose
      return;
    }
    // the store's sync writes the scene model; the held pose ends. An equal sync commits
    // nothing and releases nothing, so revert when the store keeps the node where it is.
    if (!store.movePlaced(part.node, pose)) session.input.revert(part);
  }, { signal });
}
```

## Generic-API test (spec 3.1)

| Rule | Result |
| --- | --- |
| 1. Neutral name | pass: `drag` names no product |
| 2. A precedent | pass: [Babylon.js PointerDragBehavior (startDrag, onDragObservable, validateDrag)](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/core/src/Behaviors/Meshes/pointerDragBehavior.ts) (row `G1`); [jQuery UI draggable (drag, revert)](https://api.jqueryui.com/draggable/) (row `G1`) |
| 3. Two apps, or labs | pass: 15 apps (ledger: app-04c2774145, app-37356bf09f, app-4762caacdc, app-fc07850558, app-fc306ec3dc; fleet cross-check: app-04c2774145, app-37356bf09f, app-4762caacdc, app-510d750508, app-59c5b5751e, app-b362a81c85, app-bcb7ef21da, app-c1384707fc and 7 more) |
| 4. The cheaper fix first | decided per need in Appendix B: rows `G1`, `K47` at tier labs, stable (a member only where no rule, data, asset or app code serves the need) |
