@weasel-js/routing 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2446 @@
1
+ import { CapabilityTag } from '@weasel-js/modes';
2
+ import * as react from 'react';
3
+ import { ReactNode } from 'react';
4
+ import { CursorSpec } from '@weasel-js/cursor';
5
+ import { ClaimableGesture, GestureSpec, InputEvent, TargetSpec, ParsedModifiers, GestureName, IngestItem } from '@weasel-js/gestures';
6
+ import * as react_jsx_runtime from 'react/jsx-runtime';
7
+ import { Op } from '@weasel-js/history';
8
+
9
+ /**
10
+ * The primitives the routing surface is typed in.
11
+ *
12
+ * Routing decides which action a gesture runs. To do that it has to name a
13
+ * viewport, a rect, a node id and a selection — but it never constructs,
14
+ * projects or renders any of them. So they are declared here as structural
15
+ * interfaces with no dependencies, and `@weasel-js/core` re-exports each from
16
+ * the path its own call sites have always used. Same arrangement as `Op`,
17
+ * which is declared in `@weasel-js/history` and re-exported by
18
+ * `core/ops/types`.
19
+ *
20
+ * Authority for the *behavior* behind each of these stays in core: `View` is
21
+ * what `core/viewport` maintains, `SelectionApi` is what `useSelection`
22
+ * returns, `DeviceProfile` is what `resolveDeviceProfile` detects. Changing
23
+ * one of these shapes is a change to core's contract that happens to be
24
+ * spelled here.
25
+ */
26
+ /** An opaque scene-node identifier. Branded so a bare string cannot stand in. */
27
+ type NodeId = string & {
28
+ readonly __brand: 'NodeId';
29
+ };
30
+ /**
31
+ * Viewport state. `(view.x, view.y)` is the world point currently rendered at
32
+ * the canvas top-left; `view.scale.x` / `view.scale.y` is pixels per world
33
+ * unit on each axis.
34
+ *
35
+ * The one outright 2D dead end in the routing surface: a `View` names no
36
+ * orientation, so a kernel with a camera cannot express its viewport as one.
37
+ */
38
+ interface View {
39
+ x: number;
40
+ y: number;
41
+ scale: {
42
+ x: number;
43
+ y: number;
44
+ };
45
+ }
46
+ /**
47
+ * Axis-aligned rectangle in world space. The optional `rotation` (radians,
48
+ * around the AABB center) lets chrome attach an orientation to an otherwise
49
+ * axis-aligned rect without a parallel type.
50
+ */
51
+ interface Bounds {
52
+ x: number;
53
+ y: number;
54
+ width: number;
55
+ height: number;
56
+ rotation?: number;
57
+ }
58
+ /** Selection click policy. `single` always replaces; `multi` toggles when the
59
+ * configured extend key is held, otherwise replaces. */
60
+ type SelectionMode = 'single' | 'multi';
61
+ /** Modifier key used to extend the selection in `multi` mode. */
62
+ type SelectionExtendKey = 'shift' | 'meta' | 'ctrl';
63
+ /** The selection as routing reads and writes it. */
64
+ interface SelectionApi {
65
+ /** Current selection. Re-renders trigger when this reference changes. */
66
+ current: readonly NodeId[];
67
+ /** Imperative read for use inside event callbacks (avoids stale closures). */
68
+ get(): NodeId[];
69
+ /** Replace selection. */
70
+ set(ids: NodeId[]): void;
71
+ /** Add id (multi-mode appends; single-mode replaces). */
72
+ add(id: NodeId): void;
73
+ /** Remove id from selection. */
74
+ remove(id: NodeId): void;
75
+ /** Toggle id in/out of selection. */
76
+ toggle(id: NodeId): void;
77
+ /** Clear selection. */
78
+ clear(): void;
79
+ /** True if id is selected. */
80
+ contains(id: NodeId): boolean;
81
+ /**
82
+ * Apply a click to the selection per the configured mode/extend key.
83
+ * - `single`: replaces selection with `[id]`, regardless of modifiers.
84
+ * - `multi`: with the extend key held, toggles `id` in/out of the selection;
85
+ * otherwise replaces with `[id]`.
86
+ */
87
+ applyClick(id: NodeId, modifiers: {
88
+ shift: boolean;
89
+ meta: boolean;
90
+ ctrl: boolean;
91
+ }): void;
92
+ /** Pre-built methods for spreading into an adapter that needs them. */
93
+ adapterMethods: {
94
+ getSelection: () => NodeId[];
95
+ setSelection: (ids: NodeId[]) => void;
96
+ };
97
+ }
98
+ /**
99
+ * Facts about the device the canvas is running on, as the eligibility rules
100
+ * read them (`coarsePointer`, `canHover`). Deliberately not a form-factor
101
+ * concept — there is no `isPhone` here and there should never be one.
102
+ */
103
+ interface DeviceProfile {
104
+ /** `matchMedia('(pointer: coarse)')` — the primary pointer is imprecise. */
105
+ readonly coarsePointer: boolean;
106
+ /** `matchMedia('(hover: hover)')` — the primary pointer can hover. */
107
+ readonly canHover: boolean;
108
+ /** Live device pixel ratio. */
109
+ readonly dpr: number;
110
+ /** Multiplier for handle sizes and hit radii. Derived from `coarsePointer`
111
+ * unless explicitly overridden. */
112
+ readonly targetScale: number;
113
+ }
114
+ /** Which kind of handle a recorded hit region belongs to. */
115
+ type HandleKind = 'corner' | 'rotation' | 'anchor';
116
+ /** The geometry a hit region actually tests against, as reported to the debug
117
+ * sink so the overlay can draw the real shape rather than its bounding box. */
118
+ type HitShape = {
119
+ kind: 'rect';
120
+ x: number;
121
+ y: number;
122
+ width: number;
123
+ height: number;
124
+ rotation?: number;
125
+ } | {
126
+ kind: 'circle';
127
+ cx: number;
128
+ cy: number;
129
+ r: number;
130
+ } | {
131
+ kind: 'path';
132
+ d: Path2D;
133
+ };
134
+ /**
135
+ * Where a tool reports its internal hit math so the debug overlay can draw it.
136
+ * Routing threads a sink through `ToolCtx` and never calls one.
137
+ */
138
+ interface DebugSink {
139
+ recordHitbox(id: string, kind: 'body' | 'handle' | 'rotation' | 'anchor', shape: HitShape): void;
140
+ recordHandle(id: string, position: {
141
+ x: number;
142
+ y: number;
143
+ }, kind: HandleKind): void;
144
+ recordBounds(id: string, bounds: {
145
+ x: number;
146
+ y: number;
147
+ width: number;
148
+ height: number;
149
+ }): void;
150
+ recordOrigin(id: string, point: {
151
+ x: number;
152
+ y: number;
153
+ }): void;
154
+ recordSnapCandidate(point: {
155
+ x: number;
156
+ y: number;
157
+ }, accepted: boolean): void;
158
+ recordLayer(id: string, label: string, space: 'world' | 'screen', index: number): void;
159
+ /** Clears every non-snap array. Called at the start of each Canvas render. */
160
+ beginFrame(): void;
161
+ /** Clears the snap array. Called at gesture end. */
162
+ clearSnap(): void;
163
+ }
164
+ /**
165
+ * Snapshot of modifier-key state at dispatch. Distinct from
166
+ * `EventModifiers` in `@weasel-js/gestures`, which mirrors the DOM's
167
+ * `altKey`/`ctrlKey` spelling; this is the kit-facing one.
168
+ */
169
+ interface ModifierState {
170
+ alt: boolean;
171
+ shift: boolean;
172
+ meta: boolean;
173
+ ctrl: boolean;
174
+ }
175
+ /** Which edges a resize gesture pins. `free` means the axis is unconstrained. */
176
+ type ResizeAnchor = {
177
+ x: 'min' | 'max' | 'free';
178
+ y: 'min' | 'max' | 'free';
179
+ };
180
+
181
+ /**
182
+ * Live state read by rule evaluation. Built once per frame on the consuming
183
+ * surface — chrome-caps, the affordance pipeline, the dispatcher's
184
+ * eligibility filter — and discarded.
185
+ *
186
+ * Adding a new field is additive: existing rules don't change, new
187
+ * selector atoms can read it.
188
+ */
189
+ interface RuleCtx {
190
+ readonly focused: boolean;
191
+ readonly selection: readonly NodeId[];
192
+ readonly multiActive: boolean;
193
+ readonly modifiers: ModifierState;
194
+ readonly action: {
195
+ readonly kind: string | null;
196
+ readonly id: string | null;
197
+ };
198
+ readonly hover: NodeId | null;
199
+ /**
200
+ * How far the viewport is zoomed in, as one number — `1` at native scale.
201
+ *
202
+ * A scalar rather than the `View` this used to carry, because `zoomAtLeast`
203
+ * is the only selector that ever read one and a number is all it needs. A
204
+ * host whose viewport is a camera has no `View` to hand over and would
205
+ * otherwise have to invent one to use eligibility at all; it omits this
206
+ * instead, and `zoomAtLeast` declines rather than matching on a fiction.
207
+ */
208
+ readonly zoom?: number;
209
+ /** Active mode id. `'normal'` when no non-default mode is engaged. */
210
+ readonly mode: string;
211
+ /** Capability tags allowed by the active mode (the union of
212
+ * `ModeDefinition.allows` plus implicit tags). The `capability:`
213
+ * selector reads this to determine whether a tag is permitted. */
214
+ readonly allowedCapabilities: ReadonlySet<CapabilityTag>;
215
+ /** Whether the current selection may be resized. `<SceneCanvas>` folds
216
+ * `selectTool.resize.resizable` over the selection (true only when every
217
+ * selected node is resizable). Read by the `resizable:` selector to gate
218
+ * `selection.resize-handles`. Absent (legacy ctx builders) is treated as
219
+ * resizable — back-compat: handles show unless a consumer opts a node out. */
220
+ readonly selectionResizable?: boolean;
221
+ /** Whether a path is currently in anchor-edit mode. Read by the
222
+ * `editingAnchors:` selector, which gates the path-edit chrome.
223
+ *
224
+ * This is deliberately a fact about state, not about permission: the
225
+ * anchor overlay and the anchor hit-test must agree, and the thing they
226
+ * must agree on is "is there an edited path right now", which no
227
+ * capability or mode id answers. A mode that allows `edits-anchors`
228
+ * with nothing being edited should draw no anchors. Absent is treated
229
+ * as false. */
230
+ readonly editingAnchors?: boolean;
231
+ /** Device facts — pointer coarseness, hover capability, density.
232
+ *
233
+ * Absent (legacy ctx builders) is treated as
234
+ * {@link DEFAULT_DEVICE_PROFILE}: a fine pointer that can hover, at
235
+ * density 1. That is what the kit assumed before this field existed, so
236
+ * an absent profile is behavior-preserving by construction. */
237
+ readonly device?: DeviceProfile;
238
+ }
239
+ /** The live state a `RuleCtx` is assembled from — the chrome context plus
240
+ * what the active mode allows. */
241
+ interface BuildRuleCtxArgs {
242
+ focused: boolean;
243
+ selection: readonly NodeId[];
244
+ multiActive: boolean;
245
+ modifiers: ModifierState;
246
+ action: {
247
+ kind: string | null;
248
+ id: string | null;
249
+ };
250
+ hover: NodeId | null;
251
+ /** See {@link RuleCtx.zoom}. */
252
+ zoom?: number;
253
+ mode: string;
254
+ allowedCapabilities: ReadonlySet<CapabilityTag>;
255
+ /** Optional — omitted means "resizable" (handles show). See {@link RuleCtx}. */
256
+ selectionResizable?: boolean;
257
+ /** Optional — omitted means "no path is being anchor-edited". */
258
+ editingAnchors?: boolean;
259
+ /** Optional — omitted means {@link DEFAULT_DEVICE_PROFILE}. */
260
+ device?: DeviceProfile;
261
+ }
262
+ /**
263
+ * Capability set to assume when no mode registry is wired.
264
+ *
265
+ * "No modes configured" means *everything the default mode permits*, not
266
+ * *nothing permitted*. The empty set is the wrong default: it makes every
267
+ * `capability:` rule evaluate false, so a kit default written the
268
+ * recommended way (capability rules outlive new modes; mode rules have to
269
+ * be edited) would silently hide its chrome for every consumer that hasn't
270
+ * opted into modality. `NORMAL.allows` plus the implicit tags is what a
271
+ * mode-less canvas actually behaves like.
272
+ */
273
+ declare const DEFAULT_ALLOWED_CAPABILITIES: ReadonlySet<CapabilityTag>;
274
+ /** Gather the current canvas and mode state into the context that action
275
+ * eligibility and chrome-visibility rules are evaluated against. */
276
+ declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
277
+
278
+ /**
279
+ * A selector is a conjunction of key/value tests. Multiple keys at the same
280
+ * level AND together. Each key maps to a selector primitive in the evaluator.
281
+ */
282
+ interface Selector {
283
+ selection?: {
284
+ is?: number;
285
+ atLeast?: number;
286
+ empty?: boolean;
287
+ };
288
+ mode?: string | {
289
+ not: string;
290
+ } | {
291
+ in: readonly string[];
292
+ };
293
+ capability?: CapabilityTag | readonly CapabilityTag[] | {
294
+ in: readonly CapabilityTag[];
295
+ } | {
296
+ not: CapabilityTag;
297
+ };
298
+ gesturing?: boolean;
299
+ actionIs?: string;
300
+ modifierHeld?: keyof ModifierState;
301
+ focused?: boolean;
302
+ hovering?: boolean;
303
+ hoveringSelected?: boolean;
304
+ zoomAtLeast?: number;
305
+ /** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
306
+ * mode. Absent flag is treated as `false`. */
307
+ editingAnchors?: boolean;
308
+ /** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
309
+ * (resizable), so `{ resizable: true }` passes for legacy ctx builders
310
+ * that don't compute it. */
311
+ resizable?: boolean;
312
+ /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
313
+ * (touch, most styluses). Absent device is treated as `false`. */
314
+ coarsePointer?: boolean;
315
+ /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
316
+ * device is treated as `true`. */
317
+ canHover?: boolean;
318
+ }
319
+ /**
320
+ * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
321
+ * over `Selector` leaves. `when` is the escape hatch — its closure is
322
+ * opaque to introspection and should be avoided when a declarative form
323
+ * exists. Empty `all` is true; empty `any` is false.
324
+ */
325
+ type Rule = Selector | {
326
+ all: readonly Rule[];
327
+ } | {
328
+ any: readonly Rule[];
329
+ } | {
330
+ not: Rule;
331
+ } | {
332
+ when: (ctx: RuleCtx) => boolean;
333
+ };
334
+ /** Constant rules. Kept here so they have a single source. */
335
+ declare const ALWAYS: Rule;
336
+ /** A rule that never passes. `any` of nothing is false. */
337
+ declare const NEVER: Rule;
338
+ /**
339
+ * Render a {@link Rule} as a short human-readable string, for inspectors and
340
+ * diagnostics that need to say WHICH rule decided something.
341
+ *
342
+ * ```
343
+ * { capability: 'edits-page' } → capability:edits-page
344
+ * { all: [{ mode: 'normal' }, { focused: true }] } → all(mode:normal, focused:true)
345
+ * { not: { selection: { empty: true } } } → not(selection:empty=true)
346
+ * { when: function hasPath() { … } } → when(hasPath)
347
+ * ```
348
+ *
349
+ * Never throws and always terminates — unlike `JSON.stringify`, which throws
350
+ * on a cyclic value and renders the `when` arm as a useless `{}`.
351
+ */
352
+ declare function describeRule(rule: Rule): string;
353
+ /** Evaluate a rule against a context. Pure, and cheap enough to run per
354
+ * frame for every piece of chrome. */
355
+ declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
356
+ /**
357
+ * Composable eligibility predicate with a fluent surface. Carries its
358
+ * underlying `Rule` tree at `.rule` so the dispatcher's eligibility filter and
359
+ * the chrome resolver can introspect and share trees.
360
+ *
361
+ * Callable form `cond(ctx)` evaluates the tree against ctx. The fluent methods
362
+ * return new Conditions wrapping new trees.
363
+ *
364
+ * **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
365
+ * `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. Mix `.and` and `.or`
366
+ * only when you mean left-to-right evaluation. For grouped disjunction, name
367
+ * the subexpression or use the top-level `or(...)`.
368
+ */
369
+ interface Condition {
370
+ (ctx: RuleCtx): boolean;
371
+ readonly rule: Rule;
372
+ /** `this && other` */
373
+ and(other: Condition | Rule): Condition;
374
+ /** `this || other` */
375
+ or(other: Condition | Rule): Condition;
376
+ /** `this && !other` */
377
+ andNot(other: Condition | Rule): Condition;
378
+ /** `this || !other` */
379
+ orNot(other: Condition | Rule): Condition;
380
+ }
381
+
382
+ /** The detected half of a profile — everything except the derived scale. */
383
+ type DetectedDeviceFacts = Omit<DeviceProfile, 'targetScale'>;
384
+ /**
385
+ * Handle/hit multiplier applied on a coarse pointer.
386
+ *
387
+ * 8px handle → 14px; 24px rotation distance → 42px. Counting the
388
+ * surrounding grab zone, that lands in the Apple HIG 44pt / Material 48dp
389
+ * minimum-touch-target band. One constant so it is tunable in one place.
390
+ */
391
+ declare const COARSE_TARGET_SCALE = 1.75;
392
+ /**
393
+ * Fold consumer overrides over detected facts and derive `targetScale`.
394
+ *
395
+ * `targetScale` is re-derived AFTER the merge, so an override of
396
+ * `coarsePointer` alone scales the chrome as expected. An explicit
397
+ * `targetScale` override wins over the derivation.
398
+ */
399
+ declare function resolveDeviceProfile(detected: DetectedDeviceFacts, overrides?: Partial<DeviceProfile>): DeviceProfile;
400
+ /**
401
+ * Assumed when `matchMedia` is unavailable (SSR, jsdom) and used as the
402
+ * absent-means value for `RuleCtx.device`.
403
+ *
404
+ * Derived from `resolveDeviceProfile` rather than hand-written, so it can
405
+ * never drift from what the function would compute for the same facts. A
406
+ * mouse-like device is the safe default: it is what the kit has always
407
+ * assumed, so an absent profile changes nothing for existing consumers.
408
+ */
409
+ declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
410
+
411
+ /**
412
+ * Invoker — pluggable invocation strategy for an Action.
413
+ *
414
+ * An Action's `invoker` field describes how it's run. `immediate` invokers
415
+ * fire once (delete, align, undo). `ongoing` invokers open a phase machine
416
+ * driven by the dispatcher (move, resize, pinch-zoom).
417
+ *
418
+ * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`
419
+ * § "Types" for the full design.
420
+ */
421
+
422
+ /** A 2D point in either world or screen coordinates. */
423
+ interface Point2 {
424
+ x: number;
425
+ y: number;
426
+ }
427
+ /**
428
+ * Information about which UI affordance was hit at pointerdown.
429
+ *
430
+ * Populated by the dispatcher when the `affordanceAt` thunk is provided to
431
+ * `useGestureDispatcher`. Tools / action invokers that only fire on a specific
432
+ * affordance (e.g. a resize handle) use this field as a guard — if the
433
+ * affordance is absent or is the wrong kind, they return `{}` and let other
434
+ * bindings handle the drag.
435
+ *
436
+ * `kind` is a discriminator string:
437
+ * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
438
+ * `'handle:bottom-right'` — corner resize handles.
439
+ * - `'rotate-handle'` — the rotation affordance.
440
+ * - `'anchor:N'` — a path anchor at index N.
441
+ *
442
+ * `fixedPoint` is the world-space point that should remain stationary during
443
+ * the gesture. For resize handles this is the opposite (diagonally fixed)
444
+ * corner; for rotate it is the pivot.
445
+ *
446
+ * `targetIds` are the node ids this affordance belongs to.
447
+ */
448
+ interface AffordanceHit {
449
+ /** Discriminator string, e.g. `'handle:bottom-right'`. */
450
+ kind: string;
451
+ /** Id of whatever produced this hit — a kit affordance's `id`, or the
452
+ * registered layer's id. Read only by the dispatcher's dead-claim warning today. */
453
+ owner?: string;
454
+ /** `'exclusive'` means no binding may act on this point unless its target
455
+ * consults the affordance. `'shared'` (the default) competes on scope and
456
+ * specificity as bindings always have. */
457
+ strength?: 'exclusive' | 'shared';
458
+ /** Which gestures an exclusive claim bars. Omitted bars all of them. */
459
+ claimedKinds?: readonly ClaimableGesture[];
460
+ /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
461
+ fixedPoint?: {
462
+ x: number;
463
+ y: number;
464
+ };
465
+ /** Which nodes this affordance belongs to. */
466
+ targetIds?: string[];
467
+ /** Set when `kind` matches `'handle:*'`. Identifies which corner stays
468
+ * fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
469
+ * Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
470
+ * leave this undefined. */
471
+ anchor?: ResizeAnchor;
472
+ /** Cursor to show while the pointer hovers this affordance (no
473
+ * gesture in flight). Consumed by the hover-cursor pump in
474
+ * `useGestureDispatcher`; unset = the pump falls through to
475
+ * action-cursor prediction, then to the active tool's cursor. */
476
+ cursor?: CursorSpec;
477
+ /**
478
+ * Free-form payload from whatever produced the hit, carried through to the
479
+ * matching action untouched.
480
+ *
481
+ * Kit affordances describe themselves fully in the fields above and leave
482
+ * this unset. It exists for affordances the kit doesn't know the shape of —
483
+ * a registered layer's own chrome, where the hit-test already resolved
484
+ * *which* of its pieces was hit and the action would otherwise have to
485
+ * redo that work. `@weasel-js/hud` passes the hit widget here.
486
+ */
487
+ payload?: unknown;
488
+ }
489
+ /**
490
+ * One accumulated point on a drag trail: world-space position plus whatever
491
+ * stylus state the originating `PointerEvent` carried.
492
+ *
493
+ * The stylus fields are absent for mouse/touch on browsers that don't report
494
+ * them, and for synthetic events. Consumers that want pressure-driven output
495
+ * (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
496
+ * their `insert` dep receives — see `apps/site/demos/VertexWidthsDemo.tsx`.
497
+ */
498
+ interface DragSample extends Point2 {
499
+ /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
500
+ pressure?: number;
501
+ /** Degrees, ±90. Zero for mouse/touch. */
502
+ tiltX?: number;
503
+ /** Degrees, ±90. Zero for mouse/touch. */
504
+ tiltY?: number;
505
+ }
506
+ /** Per-invocation runtime context the dispatcher hands to an Invoker.
507
+ * Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
508
+ * populated only for matching gesture kinds. */
509
+ interface InvocationCtx {
510
+ world: Point2;
511
+ /**
512
+ * The pointer in client/screen pixels, for an action that has to place DOM
513
+ * at the pointer. Absent on every invocation whose event carries no client
514
+ * coordinates — a keystroke, a UI-driven trigger, a synthetic probe — so
515
+ * read it with a fallback rather than assuming a point.
516
+ *
517
+ * A view-mutating drag wants `drag.screenDelta`, not the difference of two
518
+ * of these: world deltas are self-referential mid-pan, and so is any delta
519
+ * taken across a view change.
520
+ */
521
+ screen?: Point2;
522
+ modifiers: ModifierState;
523
+ deps: ActionDeps;
524
+ drag?: {
525
+ start: Point2;
526
+ current: Point2;
527
+ delta: Point2;
528
+ /**
529
+ * Drag delta in client/screen coordinates (CSS pixels from the drag
530
+ * origin). Use this — never `delta` — for any action whose effect
531
+ * mutates the viewport itself (pan, view-zoom), because world-space
532
+ * deltas become self-referential as the view shifts mid-drag.
533
+ *
534
+ * Populated when the dispatcher received `clientX`/`clientY` on the
535
+ * underlying pointer events. Absent for legacy callers that don't
536
+ * provide them.
537
+ */
538
+ screenDelta?: Point2;
539
+ affordance?: AffordanceHit;
540
+ /**
541
+ * Full pointermove history for the current drag, in world space, with
542
+ * per-sample stylus state when the browser reported it.
543
+ * Accumulated by the dispatcher on every `pointermove` pump event.
544
+ * Available only during `onMove` and `onEnd` calls (not on `start`).
545
+ * Used by `lassoSelectAction` to build its polygon vertex list and by
546
+ * `insertAction`'s pencil kind to carry the freehand stroke.
547
+ */
548
+ points?: DragSample[];
549
+ };
550
+ wheel?: {
551
+ deltaX: number;
552
+ deltaY: number;
553
+ deltaZ: number;
554
+ };
555
+ multiTouch?: {
556
+ centroid: Point2;
557
+ spread: number;
558
+ rotation: number;
559
+ /**
560
+ * Pinch-zoom geometry. Populated by the dispatcher when a multitouch
561
+ * handle is in flight and a pointermove-pump fires.
562
+ * `startSpread` is the spread at the moment the gesture began.
563
+ * `currentSpread` is the spread at the current frame.
564
+ */
565
+ pinch?: {
566
+ startSpread: number;
567
+ currentSpread: number;
568
+ centroid: Point2;
569
+ };
570
+ };
571
+ key?: {
572
+ key: string;
573
+ repeat: boolean;
574
+ };
575
+ /**
576
+ * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
577
+ * UI-driven ongoing actions (color picker, opacity slider) so handles can
578
+ * read the current value on `start` and updated values on `onMove`. The
579
+ * gesture dispatcher does not populate this field; gesture-driven actions
580
+ * receive params via `BindingOpts.params` on `start` (the `opts` arg).
581
+ */
582
+ params?: Record<string, unknown>;
583
+ }
584
+ /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
585
+ * `opts` field and passes to `OngoingInvoker.start`. Today carries
586
+ * behaviors; extensible. */
587
+ interface BindingOpts {
588
+ /** Gesture behaviors the action's own invoker runs. Routing carries the
589
+ * array to the action and never reads an element, so the element type is
590
+ * open here; `@weasel-js/core` narrows it to `ActionBehavior<unknown,
591
+ * unknown, unknown>[]`, which is what a behavior is authored against. */
592
+ behaviors?: unknown[];
593
+ /** Per-binding action parameters. The action's invoker reads
594
+ * these via the second arg to `run` (or via InvocationCtx for ongoing
595
+ * invokers, when needed). Loose typing (Record<string, unknown>) for
596
+ * now; consider per-action typing later via BindingOpts<A>.
597
+ *
598
+ * params may also be a thunk evaluated each time the
599
+ * dispatcher (or invoker) needs the value. Thunks let tools close over
600
+ * refs that mutate during a gesture (e.g. polygon `sides` adjusted
601
+ * mid-drag via ArrowUp). For ongoing invokers that want the latest
602
+ * values at commit, the invoker can re-call the thunk inside `onEnd`
603
+ * via `resolveParams(opts?.params)`. */
604
+ params?: Record<string, unknown> | (() => Record<string, unknown>);
605
+ }
606
+ /** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
607
+ * needed). Returns `undefined` for absent params. */
608
+ declare function resolveParams(params: BindingOpts['params']): Record<string, unknown> | undefined;
609
+ /** Convention-shaped action dependencies bag. Actions declare which
610
+ * contexts they consume; the dispatcher composes them per call.
611
+ * Consumer-side contexts (e.g. ColorContext) plug in by extending. */
612
+ interface ActionDeps {
613
+ selection?: unknown;
614
+ view?: unknown;
615
+ scene?: unknown;
616
+ pointer?: unknown;
617
+ activeTool?: unknown;
618
+ [k: string]: unknown;
619
+ }
620
+ /**
621
+ * What a `polyline` overlay is, in a word — the narrowest thing an action can
622
+ * say about how its run should look without naming a paint.
623
+ *
624
+ * The named roles are the kit's; a painter maps each to a style and falls back
625
+ * to plain chrome for one it does not know, so a package outside core can coin
626
+ * its own and still be drawn.
627
+ */
628
+ type OverlayRole =
629
+ /** Generic in-flight chrome — the marquee/lasso paint. */
630
+ 'chrome'
631
+ /** A cut the gesture would make. */
632
+ | 'cut'
633
+ /** An edge the gesture would author between two points. */
634
+ | 'connector' | (string & {});
635
+ /**
636
+ * What an in-flight gesture proposes to paint *beside* the scene, returned by
637
+ * `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
638
+ * shape being dragged out, the line a slice would cut along.
639
+ *
640
+ * Every variant is world geometry and nothing else. No `DrawCommand`, no
641
+ * color, no coordinate space to choose: an action that reached for any of
642
+ * those would be publishing core's own 2D vocabulary, and a surface rendering
643
+ * the same gesture some other way could not read it. `resolveOverlays`
644
+ * normalizes this into `ResolvedOverlay`, which is what a painter consumes —
645
+ * `useDispatcherOverlayLayer` is core's, and it owns every style decision.
646
+ */
647
+ type OngoingOverlay = {
648
+ kind: 'marquee';
649
+ start: {
650
+ x: number;
651
+ y: number;
652
+ };
653
+ current: {
654
+ x: number;
655
+ y: number;
656
+ };
657
+ shiftHeld: boolean;
658
+ } | {
659
+ kind: 'lasso';
660
+ vertices: ReadonlyArray<{
661
+ x: number;
662
+ y: number;
663
+ }>;
664
+ current: {
665
+ x: number;
666
+ y: number;
667
+ };
668
+ shiftHeld: boolean;
669
+ } | {
670
+ /**
671
+ * An open run of world points the gesture is proposing — the slice
672
+ * action's cut line, `@weasel-js/diagram`'s routed connector.
673
+ *
674
+ * Geometry only, like every other variant: `role` is the whole of what
675
+ * an action says about appearance, and the painting surface is what
676
+ * turns it into a stroke. An action that picked its own color would be
677
+ * unpaintable by any renderer but the one it had in mind.
678
+ */
679
+ kind: 'polyline';
680
+ /** At least two points. Anything shorter is dropped in resolution. */
681
+ points: ReadonlyArray<{
682
+ x: number;
683
+ y: number;
684
+ }>;
685
+ /** What the run means. Omitted reads as {@link OverlayRole} `'chrome'`. */
686
+ role?: OverlayRole;
687
+ } | {
688
+ /**
689
+ * Live insert-drag preview — dispatched by `insertAction` while the
690
+ * user is dragging out a new shape. Pre-commit there is no scene node
691
+ * to ghost via `previewIds()`/`previewPose()`, so insert paints its
692
+ * preview through the dispatcher overlay layer instead.
693
+ *
694
+ * `shape` is the kit's built-in insert kind. `bounds` is the AABB of
695
+ * the current drag (start/current normalized). `extras` is the
696
+ * per-kind extras the action already collected — the overlay
697
+ * renderer rebuilds the shape using the same path builders the
698
+ * commit factory uses, so the preview matches the eventual node.
699
+ *
700
+ * `extras` is opaque (`unknown`) at the union level; the overlay
701
+ * renderer narrows on `shape` and casts the field shape it expects.
702
+ */
703
+ kind: 'insertPreview';
704
+ /** Which preview the dispatcher's overlay draws. The kit's own shape
705
+ * kinds are the ones it knows how to render; a consumer-defined kind
706
+ * is legal and simply gets no live preview. */
707
+ shape: string;
708
+ bounds: {
709
+ x: number;
710
+ y: number;
711
+ width: number;
712
+ height: number;
713
+ };
714
+ extras: unknown;
715
+ /** World-space point to paint a small "anchor" dot at. Sells the
716
+ * click point as the drag's anchor — particularly useful for
717
+ * radial shapes (polygon/star) where no vertex sits on the
718
+ * click point, and for any shape in center mode where the dot
719
+ * marks the center the shape grows around. */
720
+ anchorPoint?: {
721
+ x: number;
722
+ y: number;
723
+ };
724
+ };
725
+ /** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
726
+ * `onMove` on subsequent input events of the same gesture and calls
727
+ * `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
728
+ * on pointercancel / blur / escape). */
729
+ interface OngoingHandle {
730
+ /**
731
+ * Optional logical action kind — a stable, human-readable tag the
732
+ * dispatcher exposes via `getActiveAction()` for chrome-visibility
733
+ * rules and any other surface that wants to react to "what action
734
+ * is currently in flight" without inspecting handles directly.
735
+ *
736
+ * Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
737
+ * `'pan'`, `'pinch'`.
738
+ *
739
+ * Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
740
+ * `key-held-Space`, etc.) which keys per-pointer state and is not
741
+ * meaningful to consumers.
742
+ *
743
+ * When omitted, the action is "anonymous" — `getActiveAction().kind`
744
+ * reports `null` even though a handle is in flight. This is fine for
745
+ * actions that don't have visible chrome of their own.
746
+ */
747
+ kind?: string;
748
+ onMove?(ctx: InvocationCtx): void;
749
+ onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
750
+ /**
751
+ * Optional preview surface — dispatcher-side ghost overlay.
752
+ *
753
+ * An ongoing-action implementation may populate `previewIds()` +
754
+ * `previewPose(id)` to expose its in-flight preview state for the
755
+ * canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
756
+ * top of the committed scene during the gesture.
757
+ *
758
+ * Returning `null` (or omitting the method entirely) means "no preview
759
+ * this gesture" — the canvas will skip this handle as a source.
760
+ *
761
+ * Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
762
+ * pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
763
+ * returns the interim pose for one of those ids (shape opaque — the
764
+ * canvas casts to its `TPose` parameter). The preview-ghost layer
765
+ * merges all sources via first-non-null semantics, with tool-side
766
+ * previews taking precedence over dispatcher-side (preserves
767
+ * backwards-compat during the registry-unification migration).
768
+ */
769
+ previewIds?(): Iterable<string> | null;
770
+ previewPose?(id: string): unknown | null;
771
+ /**
772
+ * Subset of `previewIds()` the ghost layer paints at full opacity. The
773
+ * ghost alpha says "this is in flight under the pointer"; a node the
774
+ * gesture merely displaces — a layout sibling reflowing into its
775
+ * destination slot — is not, and reads better settled. Honored at
776
+ * subtree-root granularity.
777
+ */
778
+ previewOpaqueIds?(): Iterable<string> | null;
779
+ /**
780
+ * When `false`, the preview-ghost layer paints the ghost AND the
781
+ * source node stays visible at its committed pose. Defaults to
782
+ * `true` (move/resize/rotate semantics: ghost replaces the source
783
+ * during the gesture). Clone overrides to `false` so the original
784
+ * stays put and the ghost appears at the drag target.
785
+ */
786
+ previewHidesSource?: boolean;
787
+ /**
788
+ * Optional per-id preview *data*. Falls back to the committed
789
+ * `node.data` when null/absent. Use when the gesture mutates
790
+ * `node.data` (e.g. anchor-edit on nodes that store the polygon on
791
+ * `data.path`) rather than (or in addition to) the pose. The preview-
792
+ * ghost layer assembles a synthetic node from `{ ...node, pose:
793
+ * previewPose ?? node.pose, data: previewData ?? node.data }` before
794
+ * calling the scene slot's `drawOne`.
795
+ *
796
+ * Sources compose first-non-null per axis: an action can emit only
797
+ * `previewPose` (translation), only `previewData` (data-only edit),
798
+ * or both (pose + data both change, e.g. anchor drag on a data.path
799
+ * node where the bounds shift).
800
+ */
801
+ previewData?(id: string): unknown | null;
802
+ /**
803
+ * Optional chrome surface — dispatcher-side overlay layer.
804
+ *
805
+ * An ongoing-action implementation may populate `overlay()` to expose a
806
+ * non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
807
+ * `useDispatcherOverlayLayer` to paint while the gesture is in flight.
808
+ * Returning `null` (or omitting the method) means "no overlay this
809
+ * gesture" — the canvas will skip this handle as a chrome source.
810
+ *
811
+ * Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
812
+ * which paints displaced scene-node silhouettes. Marquee and lasso
813
+ * gestures don't displace any node, but still need on-screen feedback.
814
+ */
815
+ overlay?(): OngoingOverlay | null;
816
+ }
817
+ /** Fire-once invocation. Runs to completion synchronously (or fires off an
818
+ * async side-effect; the registry doesn't wait). */
819
+ interface ImmediateInvoker {
820
+ timing: 'immediate';
821
+ /** `params` carries the matched binding's opts.params. Invoked from the
822
+ * command palette, or anywhere else with no per-binding context, `params`
823
+ * is undefined; descriptors should default to a sensible variant. */
824
+ run(deps: ActionDeps, params?: Record<string, unknown>): void;
825
+ }
826
+ /** Phase-machine invocation. `start` opens the phase and returns the handle
827
+ * the dispatcher pumps. */
828
+ interface OngoingInvoker {
829
+ timing: 'ongoing';
830
+ start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
831
+ }
832
+ /** Pluggable invocation strategy for an Action. Future variants
833
+ * (`longPress`, `twoStage`, `modal`) extend this union without touching
834
+ * the `Action` type. */
835
+ type Invoker = ImmediateInvoker | OngoingInvoker;
836
+
837
+ /**
838
+ * GestureBinding — connects a GestureSpec to an Action id (with per-binding
839
+ * options). Tools own arrays of these on their `bindings` field; ambient
840
+ * gesture-bindings are registered globally.
841
+ *
842
+ * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
843
+ */
844
+
845
+ /** An interaction: a gesture spec composed with the id of the action it
846
+ * invokes. Tools declare arrays of these; the dispatcher matches an incoming
847
+ * input event against them and runs the winner's action. */
848
+ interface GestureBinding {
849
+ spec: GestureSpec;
850
+ actionId: string;
851
+ opts?: BindingOpts;
852
+ }
853
+ /**
854
+ * @experimental
855
+ * A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
856
+ * (no per-binding opts) or an object form that pairs a spec with
857
+ * `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
858
+ * Use the object form when two bindings for the same action differ only in
859
+ * a runtime parameter — the dispatcher extracts `opts.params` and passes
860
+ * them to `ImmediateInvoker.run` as its second argument.
861
+ */
862
+ type BoundGesture = GestureSpec | {
863
+ spec: GestureSpec;
864
+ opts: BindingOpts;
865
+ };
866
+ /**
867
+ * @experimental
868
+ * The whole of an action `actionBindings` reads. Routing needs this much and
869
+ * none of `Action`'s palette fields, so it is the narrowest type the
870
+ * dispatcher's binding walk can be written against.
871
+ */
872
+ interface BindingSource {
873
+ id: string;
874
+ /** The gesture-spec form of the binding, read by the gesture dispatcher.
875
+ * May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
876
+ * or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
877
+ * `{ spec, opts }` — use the object form for parametric actions where two
878
+ * bindings for the same action differ only by `opts.params` (e.g. `flip`
879
+ * with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
880
+ * passes them to `ImmediateInvoker.run` as its second argument. */
881
+ defaultBinding?: GestureSpec | BoundGesture[];
882
+ }
883
+ /**
884
+ * @experimental
885
+ * Flatten an action's `defaultBinding` into `GestureBinding`s. A bare
886
+ * `GestureSpec` has `kind` at top level; the object form has `spec`.
887
+ */
888
+ declare function actionBindings(action: BindingSource): GestureBinding[];
889
+
890
+ /**
891
+ * @experimental
892
+ * Closed enum of reasons an action might report itself as disabled. The
893
+ * consumer (palette, menu, etc.) maps these symbolic values to display
894
+ * strings via its own label map.
895
+ */
896
+ declare const ActionDisabledReason: {
897
+ readonly SelectionRequired: "selection-required";
898
+ readonly SceneEmpty: "scene-empty";
899
+ readonly NotApplicable: "not-applicable";
900
+ /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
901
+ readonly PredicateThrew: "predicate-threw";
902
+ };
903
+ /** Why an action is unavailable right now. */
904
+ type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDisabledReason];
905
+ /**
906
+ * @experimental
907
+ * What a palette, menu or toolbar renders for an action. No part of this is
908
+ * read during dispatch.
909
+ */
910
+ interface ActionPresentation {
911
+ label: string;
912
+ /** Inline-SVG icon for palette / toolbar surfaces. Mirrors
913
+ * `ToolPresentation.icon` so a generic `<ActionBar>` can render from
914
+ * action metadata the same way `<ToolPalette>` renders from tool
915
+ * metadata. May be a static `ReactNode` or a function (rare; useful
916
+ * for state-aware icons like a "lock" toggle). */
917
+ icon?: ReactNode | (() => ReactNode);
918
+ /** Grouping key for palette/menu surfaces. Free-form string; the kit
919
+ * ships defaults for `'align'` (six edges/centers), `'distribute'`
920
+ * (two axes), and recommends `'pathfinder'` for boolean ops. */
921
+ group?: string;
922
+ /** Display override for the keyboard shortcut. When omitted, palette
923
+ * surfaces derive a label from `defaultBinding` via their own
924
+ * formatter. */
925
+ shortcut?: string;
926
+ }
927
+ /**
928
+ * @experimental
929
+ * What routing consults: the bindings that reach the action, the deps its
930
+ * invoker reads, the gates it passes, and the cursors it claims.
931
+ */
932
+ interface ActionDispatch extends BindingSource {
933
+ /** Names of the deps this action's invoker reads (keys of `DepSchema`).
934
+ * The dispatcher (and `trigger`, when `requires` is present) resolves
935
+ * each name against the `DepRegistry` at invocation time and passes the
936
+ * resulting bag to the invoker. Dev builds warn when the invoker reads a
937
+ * dep it didn't declare here — see `buildDepsFromRequires`. */
938
+ requires?: readonly DepName[];
939
+ /** Pluggable invocation strategy. The gesture dispatcher routes matched
940
+ * bindings through `invoker.start` / `invoker.run` depending on timing.
941
+ * All kit-standard descriptors ship one; consumer-supplied actions
942
+ * without an invoker can still register but won't be triggered. */
943
+ invoker?: Invoker;
944
+ /** When set to `'hotkey'`, this action's `defaultBinding` rides the hotkey
945
+ * `BindingScope` instead of the ambient scope — meaning it beats any
946
+ * active-tool binding on the same input shape. Use for tool-switch
947
+ * shortcuts and global held-key triggers. Default: ambient. */
948
+ scope?: 'hotkey';
949
+ /**
950
+ * @experimental
951
+ * Optional predicate the command palette consults when rendering. Return
952
+ * `true` when the action is currently triggerable. Return a reason string
953
+ * (e.g. `'Selection required'`) when disabled — the palette greys out
954
+ * the row, skips it in keyboard nav, ignores clicks, and shows the
955
+ * reason next to the label. Keystroke dispatch (the registered binding)
956
+ * is unaffected; the action's own `run` should self-guard.
957
+ *
958
+ * **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
959
+ * must not throw. If a call throws or exceeds the budget in dev mode,
960
+ * `evaluateEnabled` logs a one-time warning per action id; throws are
961
+ * caught and treated as disabled with reason `'(predicate threw)'`.
962
+ *
963
+ * Snapshot-on-open semantics: the palette evaluates `enabled` once when
964
+ * opened and does NOT re-evaluate on selection changes while open. Live
965
+ * reactive updates are deferred — palette is short-lived.
966
+ *
967
+ * The reason set is a closed enum — to add a new reason, edit
968
+ * `ActionDisabledReason` and the consumer's display map.
969
+ *
970
+ * The optional `deps` argument is the same bag passed to
971
+ * `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
972
+ * synthesize it from the surrounding `DepRegistry` so predicates can
973
+ * inspect selection / scene / etc. Predicates that don't need deps just
974
+ * ignore the arg.
975
+ */
976
+ enabled?: (deps?: ActionDeps) => true | ActionDisabledReason;
977
+ /**
978
+ * Declarative eligibility rule, evaluated against the current
979
+ * `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
980
+ * always eligible.
981
+ *
982
+ * Accepts either a fluent `Condition` (callable with `.rule`) or a
983
+ * raw `Rule` tree; the dispatcher normalizes via `.rule` unwrap.
984
+ *
985
+ * Prefer `capability:`-based rules (e.g. `{ capability: 'transforms-selection' }`)
986
+ * over `mode:` rules — capability rules survive new modes being added
987
+ * that allow the same capability.
988
+ */
989
+ eligible?: Rule | Condition;
990
+ /**
991
+ * CSS cursor shown while the pointer hovers a spot where this action
992
+ * would win the drag. The hover-cursor pump (in `useGestureDispatcher`)
993
+ * runs `Dispatcher.resolveOnly` on each idle pointermove — the same
994
+ * match walk a real pointerdown takes — and applies the winning
995
+ * action's `cursor`, so the hint and the actual click target stay in
996
+ * sync by construction. Omitted = no override (the active tool's
997
+ * `Tool.cursor` shows). Affordance hits are resolved earlier in the
998
+ * pump via `AffordanceRegion.cursor` and never reach this field.
999
+ *
1000
+ * Static value only. Prediction runs `enabled()` but cannot run the
1001
+ * invoker, so an action that matches yet bails at `start()` (empty
1002
+ * handle) may still show its cursor — keep `enabled` accurate for
1003
+ * actions that declare one.
1004
+ */
1005
+ cursor?: CursorSpec;
1006
+ /**
1007
+ * CSS cursor shown while THIS action's ongoing handle is in flight —
1008
+ * grabbing while panning, `move` while dragging a selection, `crosshair`
1009
+ * while pulling a marquee.
1010
+ *
1011
+ * Separate from `cursor` because the two answer different questions:
1012
+ * `cursor` is a prediction ("a drag from here would pan"), this is a state
1013
+ * ("you are panning"). An action can declare either, both, or neither;
1014
+ * with only `cursor` set, the hover hint holds for the duration of the
1015
+ * gesture.
1016
+ *
1017
+ * This is where mid-gesture cursors live now. They used to come from the
1018
+ * tool side — `ViewportToolDef.engaged.cursor` for a phase-gated string,
1019
+ * or a function-form `Tool.cursor` reading the gesture scratch out of the
1020
+ * tool-routing dispatcher. Both belonged to a pipeline whose whole job was
1021
+ * being taken over by bindings, and neither could describe a cursor for an
1022
+ * action a tool doesn't own.
1023
+ */
1024
+ activeCursor?: CursorSpec;
1025
+ }
1026
+ /**
1027
+ * @experimental
1028
+ * Single registered action. v1: one binding per action.
1029
+ */
1030
+ interface Action extends ActionDispatch, ActionPresentation {
1031
+ }
1032
+ /**
1033
+ * @experimental
1034
+ * The whole of an actions registry the dispatcher reads. It walks the list
1035
+ * once per event to build its actionId lookup and never mutates it, so the
1036
+ * routing side depends on this rather than on `ActionsRegistry`, whose
1037
+ * register / mute / trigger surface is the React provider's.
1038
+ */
1039
+ interface ActionSource {
1040
+ list(): readonly Action[];
1041
+ }
1042
+
1043
+ /**
1044
+ * @experimental
1045
+ * Palette-side evaluation of `Action.enabled`. The dispatcher calls the
1046
+ * predicate raw as a gate; this is the surface that also enforces the
1047
+ * contract the predicate's JSDoc states — no throwing, under 4ms in dev.
1048
+ */
1049
+
1050
+ /**
1051
+ * @experimental
1052
+ * Result of evaluating an Action's `enabled` predicate.
1053
+ */
1054
+ interface ActionEnabledResult {
1055
+ enabled: boolean;
1056
+ reason?: ActionDisabledReason;
1057
+ }
1058
+ /**
1059
+ * @experimental
1060
+ * Safely evaluate an action's `enabled` predicate. Returns `{enabled: true}`
1061
+ * when no predicate is supplied. Catches throws and treats them as disabled
1062
+ * with reason `'(predicate threw)'`. In dev mode, warns once per action id
1063
+ * when a single call exceeds 4ms (single frame at 240fps — generous; real
1064
+ * predicates should be sub-millisecond).
1065
+ */
1066
+ declare function evaluateEnabled(action: Action, deps?: ActionDeps): ActionEnabledResult;
1067
+
1068
+ /** Holds the live sources an action's declared dependencies resolve to.
1069
+ * Sources are thunks, read at invocation time, so an action never captures
1070
+ * stale state. */
1071
+ interface DepRegistry {
1072
+ register<K extends DepName>(name: K, source: () => DepSchema[K]): () => void;
1073
+ get<K extends DepName>(name: K): DepSchema[K] | undefined;
1074
+ }
1075
+ /** Provides the dep registry for a canvas. `<SceneCanvas>` mounts one; a
1076
+ * consumer registering its own dep sources must be inside it. */
1077
+ declare function DepRegistryProvider({ children }: {
1078
+ children: ReactNode;
1079
+ }): react_jsx_runtime.JSX.Element;
1080
+ /** The dep registry in scope. Throws outside a `<DepRegistryProvider>`. */
1081
+ declare function useDepRegistry(): DepRegistry;
1082
+ /**
1083
+ * Like `useDepRegistry`, but returns `null` when no `<DepRegistryProvider>` is
1084
+ * in scope instead of throwing. Used by `useStandardActions` to preserve its
1085
+ * silent-no-op contract when neither provider is present.
1086
+ */
1087
+ declare function useOptionalDepRegistry(): DepRegistry | null;
1088
+ /** Register a live source for `name` for the lifetime of the calling
1089
+ * component. The `source` thunk is called at dispatch time and should
1090
+ * return the latest value. */
1091
+ declare function useDepSource<K extends DepName>(name: K, source: () => DepSchema[K]): void;
1092
+
1093
+ /**
1094
+ * Shared dep-bag builder — the single implementation behind both the gesture
1095
+ * dispatcher's per-invocation `buildDeps` and `ActionsRegistry.trigger`'s
1096
+ * `requires` branch. Reads the action's declared `requires`, resolves each
1097
+ * name against the `DepRegistry`, and (dev builds only) wraps the bag in a
1098
+ * Proxy that warns on undeclared reads.
1099
+ */
1100
+
1101
+ /** Build the deps bag for an action from its declared `requires`. */
1102
+ declare function buildDepsFromRequires(action: Action, depRegistry: DepRegistry): ActionDeps;
1103
+
1104
+ /** Hotkey-slot trigger key. The slot is engaged while this key is held —
1105
+ * hence "hotkey": active as long as the key is hot. `null` (or omitted)
1106
+ * means the tool is not eligible for the hotkey slot. */
1107
+ type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift';
1108
+ /** Presentation metadata for tool palettes / menus. Optional on every
1109
+ * tool — consumers that render a palette (`<ToolPalette>`) read these
1110
+ * fields to display the tool; consumers that don't can ignore them.
1111
+ *
1112
+ * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
1113
+ * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
1114
+ interface ToolPresentation<TScratch = unknown> {
1115
+ /** Human-readable label, distinct from the `id`. Falls back to `id`. */
1116
+ label?: string;
1117
+ /** Inline-SVG icon component output. May be a static `ReactNode` or a
1118
+ * function of scratch state (rare; useful for shape-aware affordances). */
1119
+ icon?: react.ReactNode | ((scratch?: TScratch) => react.ReactNode);
1120
+ /** Palette grouping key. Tools sharing a group render contiguously
1121
+ * with separators between groups. Free-form string; the kit
1122
+ * recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
1123
+ group?: string;
1124
+ /** Display override for the keyboard shortcut. When omitted the palette
1125
+ * derives one from `Tool.keybinding` via its own formatter. */
1126
+ shortcut?: string;
1127
+ }
1128
+ /**
1129
+ * When an entry's bindings are live. A set, not one value: the hand tool is
1130
+ * palette-selectable AND engaged by holding space, and both hold at once.
1131
+ */
1132
+ interface Eligibility {
1133
+ /** Selectable as the focused entry — exclusive, one at a time. */
1134
+ focus?: boolean;
1135
+ /** Also live while this key is held. */
1136
+ offhand?: HotkeyTrigger;
1137
+ /** Live regardless of what is focused. */
1138
+ always?: boolean;
1139
+ /** Live only for input this entry's own affordances produced. */
1140
+ claimed?: boolean;
1141
+ /** Modality filter, applied wherever it would otherwise be live. */
1142
+ capabilities?: CapabilityTag[];
1143
+ }
1144
+ /**
1145
+ * Where an entry's overlay sits in the layer stack, relative to the
1146
+ * selection chrome. `'top'` is the default and renders above everything;
1147
+ * the other two exist for chrome that belongs under the selection handles
1148
+ * (a snap-target highlight, say). With no selection overlay in the stack,
1149
+ * all three collapse to `'top'`.
1150
+ */
1151
+ type OverlayPosition = 'top' | 'before-selection' | 'after-selection';
1152
+ /**
1153
+ * What routing reads off an entry: when its bindings are live, which they
1154
+ * are, and the actions they name.
1155
+ */
1156
+ interface ContributionRouting {
1157
+ id: string;
1158
+ eligibility: Eligibility;
1159
+ bindings?: GestureBinding[];
1160
+ actions?: Action[];
1161
+ }
1162
+ /**
1163
+ * What an entry draws and how a palette shows it. None of it reaches the
1164
+ * dispatcher.
1165
+ */
1166
+ interface ContributionChrome<TOverlay = unknown> {
1167
+ /** One layer, or several composed in the given order.
1168
+ *
1169
+ * Routing never reads a layer — it collects the live ones and hands them
1170
+ * back in scope order. `TOverlay` is what the consuming kernel draws;
1171
+ * `@weasel-js/core` binds it to `RenderLayer<unknown>`. */
1172
+ overlay?: TOverlay | TOverlay[];
1173
+ /** Defaults to `'top'`. Applies to every layer in `overlay`. */
1174
+ overlayPosition?: OverlayPosition;
1175
+ presentation?: ToolPresentation;
1176
+ }
1177
+ /**
1178
+ * A registry entry: what it contributes, and when it is eligible. Every role
1179
+ * is optional and independent — an entry that only routes input declares only
1180
+ * `bindings` and `actions`.
1181
+ */
1182
+ interface Contribution<TOverlay = unknown> extends ContributionRouting, ContributionChrome<TOverlay> {
1183
+ /** Reflection escape hatch — the authored form, when there was one. */
1184
+ def?: unknown;
1185
+ }
1186
+
1187
+ /**
1188
+ * Configurable activation-key descriptor for tools that expose their
1189
+ * keybinding to the host (currently Lasso and Eyedropper). Captures
1190
+ * only the fields meaningful to a caller-supplied tool-select key —
1191
+ * dispatcher-internal fields (`skipInEditable`, `enabled`,
1192
+ * `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
1193
+ * not part of the configurable surface.
1194
+ */
1195
+ interface ToolKeybinding {
1196
+ /** Key or list of keys to match (case-insensitive against `event.key`). */
1197
+ key: string | readonly string[];
1198
+ /** Require Cmd (mac) / Ctrl (others). Default `false`. */
1199
+ mod?: boolean;
1200
+ /** Require Alt. Default `false`. */
1201
+ alt?: boolean;
1202
+ /**
1203
+ * Shift policy. `undefined`/`false` forbids shift, `true` requires
1204
+ * shift, `'optional'` allows either.
1205
+ */
1206
+ shift?: boolean | 'optional';
1207
+ }
1208
+ /** Modifier-key snapshot at event dispatch time. */
1209
+ type ToolModifiers = ModifierState;
1210
+ /** Per-event context passed to every channel handler. `scratch` is typed
1211
+ * via the tool's `TScratch` parameter; it survives across a single
1212
+ * gesture (pointer-down through end/cancel) and is replaced on next
1213
+ * gesture start by `initScratch()`. */
1214
+ interface ToolCtx<TScratch = unknown> {
1215
+ worldX: number;
1216
+ worldY: number;
1217
+ modifiers: ToolModifiers;
1218
+ selection: SelectionApi;
1219
+ /** Adapter/scene access — opaque at this layer; tools that need it
1220
+ * cast to a known shape. This layer doesn't constrain it. */
1221
+ adapter: unknown;
1222
+ applyOps: (ops: Op[], label: string) => void;
1223
+ /** Current viewport. Reflects camera-position semantics — see
1224
+ * `View` JSDoc. */
1225
+ view: View;
1226
+ /** Mutate the viewport. In controlled mode this calls the consumer's
1227
+ * `onViewChange`; in uncontrolled mode it updates Canvas's internal
1228
+ * state. View changes are not undoable. */
1229
+ setView: (next: View) => void;
1230
+ /** Bounding rect of the canvas element in viewport coords. Used by
1231
+ * zoom/pan tools to convert event clientX/clientY to canvas-relative
1232
+ * anchors. */
1233
+ canvasRect: DOMRect;
1234
+ /** Screen-space pointer coords relative to `canvasRect`. Useful for
1235
+ * viewport tools that pan/zoom in screen space (e.g. hand-pan
1236
+ * computes deltas in pixels, not world units). Optional — populated
1237
+ * by the dispatcher on pointer events; absent on keyboard events. */
1238
+ screenPoint?: {
1239
+ x: number;
1240
+ y: number;
1241
+ };
1242
+ /** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
1243
+ * threads its sink here so tool-internal hit math (handle hitboxes,
1244
+ * rotation handle, etc.) lands in the same overlay as Canvas's own
1245
+ * bounds/origin records. Tools should call this conditionally with `?.`. */
1246
+ debug?: DebugSink;
1247
+ scratch: TScratch;
1248
+ }
1249
+ /** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
1250
+ * `Bounds` type — the optional `rotation` field carries through so a tool
1251
+ * can report an oriented preview rect (e.g. mid-rotate). */
1252
+ type ToolBounds = Bounds;
1253
+ /**
1254
+ * The focus-declaring case of a `Contribution`: a mode the user switches
1255
+ * into, plus the hooks that only make sense for one (`initScratch`,
1256
+ * activate/deactivate, live preview, `cursor`). Everything else — bindings,
1257
+ * actions, overlay, presentation — is inherited.
1258
+ */
1259
+ interface Tool<TScratch = unknown, TOverlay = unknown> extends Contribution<TOverlay> {
1260
+ /** Optional caller-supplied key. Most built-in tools have their activation
1261
+ * key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
1262
+ * is for tools that want their activation key to be configurable by the
1263
+ * host (currently Lasso and Eyedropper). The dynamic loop in
1264
+ * `useKeybindings.ts` picks this up and appends a binding entry to the
1265
+ * consolidated `tool.activate` action (with `opts.params.toolId` set so
1266
+ * the invoker knows which tool to switch to). */
1267
+ keybinding?: ToolKeybinding;
1268
+ initScratch?: () => TScratch;
1269
+ cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
1270
+ onActivate?: (ctx: ToolCtx<TScratch>) => void;
1271
+ onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
1272
+ /** Returns the in-flight preview pose for `id` if this tool is mid-gesture
1273
+ * on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
1274
+ * reflect live gesture state without reaching into hook internals. The
1275
+ * return type is `unknown` here because the Tool interface is pose-agnostic;
1276
+ * callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
1277
+ * the use site. */
1278
+ previewPose?: (id: string) => unknown;
1279
+ /** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
1280
+ * on it; otherwise `null`. Optional companion to `previewPose` for tools that
1281
+ * can compute bounds without round-tripping through a geometry adapter. */
1282
+ previewBounds?: (id: string) => ToolBounds | null;
1283
+ /** Returns ids whose committed scene-render should be suppressed while this
1284
+ * tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
1285
+ * preview ghosts replace the committed pose). The standard scene slot
1286
+ * consults this alongside `previewPose` to avoid double-rendering. Returns
1287
+ * `null` when no gesture is in flight. */
1288
+ previewIds?: () => Iterable<string> | null;
1289
+ }
1290
+ /** Internal — which slot a tool occupies in the dispatch order. */
1291
+ type ToolSlot = 'hotkey' | 'active' | 'ambient';
1292
+ /** Internal alias for "a Tool of any scratch type" — used in registries and
1293
+ * dispatchers that hold tools of heterogeneous scratch shapes. `any` is
1294
+ * intentional: `Tool<TScratch>` is invariant in TScratch, so `Tool<unknown>`
1295
+ * is too strict for containers that accept any concrete `Tool<T>`. */
1296
+ type AnyTool = Tool<any, any>;
1297
+ /** "A tool of any scratch type that draws `TOverlay`" — the container form for
1298
+ * registries that hold heterogeneous scratch shapes but one overlay type. */
1299
+ type AnyToolOf<TOverlay> = Tool<any, TOverlay>;
1300
+
1301
+ /**
1302
+ * Pure matcher primitives live in `@weasel-js/gestures`. This file
1303
+ * re-exports them for kit-internal consumers and layers the actions-layer
1304
+ * binding-scope / matchBest logic on top.
1305
+ */
1306
+
1307
+ /** Where a binding came from, which is also its priority: a held hotkey beats
1308
+ * the active tool, which beats bindings that are always in scope. */
1309
+ type BindingScope = 'ambient' | 'active' | 'hotkey';
1310
+ /** A binding paired with where it came from, ready to be matched against an
1311
+ * event. */
1312
+ interface ScopedBinding {
1313
+ binding: GestureBinding;
1314
+ scope: BindingScope;
1315
+ /** Tool id that owns this binding — `'&'`-channel phase atoms resolve
1316
+ * to this. `null` for ambient bindings that came from a registered
1317
+ * Action with no owning tool. */
1318
+ ownerToolId: string | null;
1319
+ }
1320
+ /** The binding that won a match. */
1321
+ interface MatchResult {
1322
+ binding: GestureBinding;
1323
+ scope: BindingScope;
1324
+ /** Tool id that owns the binding — propagated from `ScopedBinding`
1325
+ * so the dispatcher can record it as the handle owner. */
1326
+ ownerToolId: string | null;
1327
+ }
1328
+ /**
1329
+ * True when a spec's target actually consults the affordance hit rather than
1330
+ * only the body classification. `kindOf` predicates are handed the hit;
1331
+ * `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
1332
+ * `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
1333
+ * `bodyTarget` / `bodyKind` and never see it — which is why chrome floating
1334
+ * over empty canvas used to read as empty canvas.
1335
+ *
1336
+ * A predicate carrying `readsAffordance: false` says the same about itself;
1337
+ * the kit's own body predicates do. Shape stays the fallback for predicates
1338
+ * that declare nothing.
1339
+ */
1340
+ declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
1341
+ /** CSS-style specificity tuple for a GestureSpec. Higher tuple wins under
1342
+ * lexicographic compare. Dimensions, in order of precedence:
1343
+ *
1344
+ * [0] target — how much the spec's target narrows; see `targetRank`.
1345
+ * [1] mods — count of required modifier keys (shift/alt/ctrl/meta/mod).
1346
+ * `'optional'` does NOT count.
1347
+ * [2] phase — how much the spec's `phase` narrows; see `phaseRank`.
1348
+ * [3] exact — per-kind tiebreak: 2 for a drop/paste spec with a
1349
+ * non-empty `types` MIME filter, else 1.
1350
+ *
1351
+ * Identical tuples fall back to registration order in the matcher's
1352
+ * stable sort, preserving the pre-specificity tiebreaker. */
1353
+ declare function specificity(spec: GestureSpec): readonly [number, number, number, number];
1354
+ /**
1355
+ * Find the best-matching binding across all scopes.
1356
+ *
1357
+ * Precedence: hotkey > active > ambient.
1358
+ * Within a scope, more-specific bindings win; same-specificity bindings keep
1359
+ * registration order (see `matchSorted`).
1360
+ * Returns null when nothing matches.
1361
+ *
1362
+ * Note: this returns only the single best match. The dispatcher uses
1363
+ * `matchSorted` so it can fall through to lower-specificity matches when a
1364
+ * higher-specificity match's `enabled()` reports disabled.
1365
+ */
1366
+ declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
1367
+ /**
1368
+ * Find every binding that matches the event, sorted by precedence
1369
+ * (best → worst). Ordering rules:
1370
+ *
1371
+ * - Scopes by priority: hotkey > active > ambient.
1372
+ * - Within a scope, more-specific bindings beat less-specific (see
1373
+ * `specificity()`).
1374
+ * - Among same-specificity bindings, first-declared wins (stable sort
1375
+ * preserves registration order — same tiebreaker as pre-specificity).
1376
+ *
1377
+ * Used by the dispatcher to implement specificity-ordered fall-through:
1378
+ * when the top match's `enabled()` returns a disabled reason (or the
1379
+ * action's `start()` returns an empty handle), the next match is tried.
1380
+ */
1381
+ declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
1382
+
1383
+ /**
1384
+ * Dispatcher orchestrator — pure module, no React, no DOM.
1385
+ *
1386
+ * Assembles `ScopedBinding[]` from the actions registry, active tool, and
1387
+ * hotkey stack; matches input events via `matchSorted`; gates each candidate
1388
+ * on `enabled()`; then invokes `immediate` or `ongoing` invokers and tracks
1389
+ * in-flight handles.
1390
+ *
1391
+ * ## Specificity-ordered fall-through
1392
+ * `matchSorted` returns every matching binding in precedence order
1393
+ * (hotkey > active > ambient, first-declared within scope). The dispatcher
1394
+ * walks that list and fires the first action whose `enabled()` returns
1395
+ * `true`. If every candidate's `enabled()` returns a disabled reason, the
1396
+ * event is unhandled. This mirrors CSS-style specificity matching with a
1397
+ * `:not(:disabled)` filter, and lets a tool declare a high-specificity
1398
+ * binding (e.g. drag-on-empty → areaSelect) that gracefully falls through
1399
+ * to a lower-specificity ambient binding (e.g. drag → viewport.dragPan)
1400
+ * when its required deps aren't wired.
1401
+ *
1402
+ * ## gestureId scheme
1403
+ * - `key-held` ongoing actions: `key-held-<key>` (e.g. `key-held- ` for Space).
1404
+ * Chosen because key-held gestures are identified by the held key alone.
1405
+ * - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, taken from
1406
+ * the originating DOM `PointerEvent`. Each physical pointer — mouse, each
1407
+ * touch, the stylus — gets its own handle slot. Events with no
1408
+ * `pointerId` (synthesized probes, programmatic drags, most tests) key to
1409
+ * `pointer-mouse`, so a single synthetic pointer behaves as it always has.
1410
+ * - `multitouch` ongoing actions: `multitouch-<fingers>`.
1411
+ * - Fallback for any other kind that triggers an ongoing invoker: `ongoing-<kind>`.
1412
+ *
1413
+ * ## Action-lookup miss behavior
1414
+ * When `matchBest` resolves a binding whose `actionId` has no entry in
1415
+ * `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
1416
+ * `'unhandled'`. The user's input gesture falls through as if unmatched.
1417
+ * This preserves input flow (nothing is swallowed silently) while flagging
1418
+ * the misconfiguration at dev time.
1419
+ */
1420
+
1421
+ /**
1422
+ * The in-flight handle slot a pointer's gestures key into.
1423
+ *
1424
+ * Exported because the React seam (`useGestureDispatcher`) has to name the
1425
+ * same slot when it asks `dispatcher.inFlight()` whether a given pointer is
1426
+ * mid-drag. That used to be the string literal `'pointer-mouse'` on both
1427
+ * sides, which was correct only because `gestureIdFor` ignored the pointer id
1428
+ * entirely — the two lies cancelled out.
1429
+ *
1430
+ * @param pointerId - `PointerEvent.pointerId`, or `undefined` for a
1431
+ * synthesized event with no originating DOM pointer.
1432
+ */
1433
+ declare function pointerGestureId(pointerId: number | undefined): string;
1434
+ /** One entry per `handleInput` call. */
1435
+ interface DispatchLogEntry {
1436
+ kind: 'dispatch';
1437
+ ts: number;
1438
+ eventKind: string;
1439
+ /** For `key` / `key-held` events, the key id (`'Escape'`, `' '`, `'a'`).
1440
+ * Lives on the event side of the entry — `fired` should describe
1441
+ * what dispatched, not which key triggered it. */
1442
+ key?: string;
1443
+ candidates: Array<{
1444
+ actionId: string;
1445
+ scope: BindingScope;
1446
+ enabledResult: boolean | string;
1447
+ }>;
1448
+ fired: string | null;
1449
+ outcome: 'handled' | 'unhandled';
1450
+ }
1451
+ /** One entry per "mode change" — a kit state transition that isn't a
1452
+ * dispatched input event but is load-bearing for understanding why an
1453
+ * input was (or wasn't) handled later. Pushed via `recordModeSwitch()`;
1454
+ * consumers include the editAnchors dep (editingId changes), and
1455
+ * could expand to active-tool / hotkey-stack / focus changes. */
1456
+ interface ModeSwitchLogEntry {
1457
+ kind: 'mode';
1458
+ ts: number;
1459
+ /** Logical mode name, e.g. `'editAnchors.editingId'`. */
1460
+ mode: string;
1461
+ from: string | null;
1462
+ to: string | null;
1463
+ /** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
1464
+ * `'selection-dropped target'`, etc. */
1465
+ detail?: string;
1466
+ }
1467
+ type TraceLogEntry = DispatchLogEntry | ModeSwitchLogEntry;
1468
+ /** Push a mode-switch record into the trace log. Safe to call from
1469
+ * anywhere; no-ops outside DEV. */
1470
+ declare function recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
1471
+ /** Everything the dispatcher must consult to route one input event: the
1472
+ * registered actions and their deps, which tool is active, which hotkeys are
1473
+ * held, and — optionally — the chrome state that action eligibility rules are
1474
+ * evaluated against. Rebuilt per event rather than held, so the dispatcher
1475
+ * itself stays stateless apart from in-flight gestures. */
1476
+ interface DispatcherContext {
1477
+ /** All registered actions; the dispatcher walks `.defaultBinding` for ambient bindings. */
1478
+ actions: ActionSource;
1479
+ /** Dep sources keyed by name. */
1480
+ depRegistry: DepRegistry;
1481
+ /** Active tool's id (from ActiveToolContext). */
1482
+ activeToolId: string;
1483
+ /** Held-hotkey stack, top of stack last. */
1484
+ hotkeyStack: readonly string[];
1485
+ /** Lookup for tool definitions. */
1486
+ toolsById: ReadonlyMap<string, Tool>;
1487
+ /** Platform flag for `mod` shorthand resolution. */
1488
+ isMac: boolean;
1489
+ /**
1490
+ * Thunk returning a fresh `RuleCtx` for the current frame — the routed
1491
+ * view's, when the surface hosts several. When it answers, the dispatcher
1492
+ * filters matched candidates by their declared `Action.eligible` rule
1493
+ * (omitted => always eligible). Absent, or answering `undefined`, applies
1494
+ * no eligibility filtering.
1495
+ */
1496
+ getRuleCtx?: () => RuleCtx | undefined;
1497
+ }
1498
+ /**
1499
+ * Filter `matches` to only those whose backing action has no `eligible`
1500
+ * rule, or whose rule evaluates true against `ruleCtx`. Exported for
1501
+ * unit testing.
1502
+ *
1503
+ * @internal
1504
+ */
1505
+ declare function filterEligible<M extends {
1506
+ binding: {
1507
+ actionId: string;
1508
+ };
1509
+ }>(matches: readonly M[], actionLookup: (id: string) => Action | undefined, ruleCtx: RuleCtx): M[];
1510
+ /**
1511
+ * Handle returned by `Dispatcher.beginUiOngoing()` for driving an
1512
+ * ongoing invoker from a UI control (color picker, slider).
1513
+ *
1514
+ * - `update(params)` rebuilds an `InvocationCtx` with the new params and
1515
+ * calls the handle's `onMove`. Safe to call many times.
1516
+ * - `end(reason)` calls `onEnd(ctx, reason)` once and removes the handle
1517
+ * from the in-flight map. Idempotent — further calls are no-ops.
1518
+ */
1519
+ interface UiOngoingControl {
1520
+ readonly gestureId: string;
1521
+ update(params?: Record<string, unknown>): void;
1522
+ end(reason: 'commit' | 'cancel'): void;
1523
+ }
1524
+ /**
1525
+ * Successful `Dispatcher.resolveOnly` prediction: the binding + action that
1526
+ * would fire if `event` were dispatched for real. `action` is the resolved
1527
+ * descriptor so callers (the hover-cursor pump) can read metadata like
1528
+ * `Action.cursor` without a second registry lookup.
1529
+ */
1530
+ interface ResolveOnlyResult {
1531
+ actionId: string;
1532
+ action: Action;
1533
+ scope: BindingScope;
1534
+ /** Tool id owning the winning binding; `null` for ambient action bindings. */
1535
+ ownerToolId: string | null;
1536
+ }
1537
+ /**
1538
+ * One candidate from `Dispatcher.resolveAll` — a binding that matched the
1539
+ * event, with why it did or didn't get to fire.
1540
+ *
1541
+ * Verdicts:
1542
+ * - `would-fire` — eligible, `enabled()` passed, and nothing above it fired.
1543
+ * At most one candidate per call carries this.
1544
+ * - `ineligible` — the action's `eligible` rule evaluated false against the
1545
+ * live `RuleCtx`. `reason` is the rule, serialized.
1546
+ * - `disabled` — `enabled()` returned a disabled reason, carried verbatim.
1547
+ * - `shadowed` — never asked, for one of two reasons: something above it
1548
+ * already fired, or it is a repeat binding of the action that itself won
1549
+ * higher in the list (several bindings may point at one action, and the
1550
+ * dispatcher runs each action at most once). A repeat of an action that was
1551
+ * already judged `ineligible` or `disabled` is NOT shadowed — it inherits
1552
+ * that action's verdict, since that is the reason it doesn't fire.
1553
+ */
1554
+ interface ResolvedCandidate {
1555
+ actionId: string;
1556
+ action: Action;
1557
+ binding: GestureBinding;
1558
+ scope: BindingScope;
1559
+ ownerToolId: string | null;
1560
+ /** The tuple from `specificity(binding.spec)`, surfaced so a reader can see
1561
+ * why one candidate outranks another rather than inferring it. */
1562
+ specificity: readonly [number, number, number, number];
1563
+ verdict: {
1564
+ kind: 'would-fire';
1565
+ } | {
1566
+ kind: 'ineligible';
1567
+ reason: string;
1568
+ } | {
1569
+ kind: 'disabled';
1570
+ reason: string;
1571
+ } | {
1572
+ kind: 'shadowed';
1573
+ };
1574
+ }
1575
+ /** Options for {@link Dispatcher.resolveAll}. */
1576
+ interface ResolveAllOptions {
1577
+ /**
1578
+ * Evaluate eligibility and `enabled()` for candidates below the winner
1579
+ * instead of short-circuiting them to `shadowed`.
1580
+ *
1581
+ * Off by default, and deliberately so: the default walk's early exit is what
1582
+ * keeps `resolveOnly`'s `enabled()` call count identical to a real dispatch,
1583
+ * and `enabled()` predicates are only contractually pure — not free.
1584
+ *
1585
+ * Turn it on for diagnostics, where "this one was outranked" is a less
1586
+ * useful answer than "this one was outranked AND would have been disabled
1587
+ * anyway". With it on, `shadowed` narrows to its precise meaning: this
1588
+ * candidate would have fired, but something above it did.
1589
+ */
1590
+ evaluateShadowed?: boolean;
1591
+ }
1592
+ /**
1593
+ * Routes input events to actions.
1594
+ *
1595
+ * For each event it assembles the bindings in scope (hotkey, then active tool,
1596
+ * then ambient), matches them in specificity order, filters by eligibility and
1597
+ * each action's `enabled` gate, and invokes the first survivor. Ongoing
1598
+ * actions — anything that runs across a drag — are kept in flight here and
1599
+ * pumped with subsequent moves until the gesture ends.
1600
+ */
1601
+ interface Dispatcher {
1602
+ /**
1603
+ * Route an input event through the binding pipeline. Returns `'handled'`
1604
+ * when a binding matched and the action invoked successfully (whether it
1605
+ * returned ops or not). Returns `'unhandled'` when no binding matched or
1606
+ * the matched action's `enabled()` returned a disabled reason.
1607
+ */
1608
+ handleInput(event: InputEvent, ctx: DispatcherContext): 'handled' | 'unhandled';
1609
+ /**
1610
+ * Predict which action `event` would route to WITHOUT invoking it. Replays
1611
+ * the same walk as `handleInput` — scope assembly, specificity-sorted
1612
+ * match, eligibility filter, per-candidate `enabled()` gate — and returns
1613
+ * the first candidate that would fire, or `null` when the event would go
1614
+ * unhandled. Pure query: no invoker runs, no in-flight state changes, no
1615
+ * trace-log entry.
1616
+ *
1617
+ * Known divergence from a real dispatch: an ongoing invoker that matches
1618
+ * but returns an empty handle at `start()` (runtime bail) makes the real
1619
+ * dispatch fall through to the next candidate; prediction cannot see that
1620
+ * and reports the bailing action. Keep `enabled()` accurate on actions
1621
+ * that rely on prediction (hover cursors).
1622
+ */
1623
+ resolveOnly(event: InputEvent, ctx: DispatcherContext): ResolveOnlyResult | null;
1624
+ /**
1625
+ * Every binding that matches `event`, in dispatch precedence order, each
1626
+ * with a verdict explaining whether it would fire. Same walk as
1627
+ * `resolveOnly` — scope assembly, specificity-sorted match, eligibility
1628
+ * check, per-candidate `enabled()` gate — without stopping at the winner
1629
+ * and without invoking anything. Nothing is dropped: candidates that
1630
+ * `resolveOnly`'s walk would filter out are kept here and labelled
1631
+ * `ineligible` instead. Pure query: no invoker runs, no in-flight state
1632
+ * changes, no trace-log entry.
1633
+ *
1634
+ * `resolveOnly` is the first `would-fire` entry of this list.
1635
+ *
1636
+ * Shares `resolveOnly`'s known divergence from a real dispatch: an ongoing
1637
+ * invoker that matches but returns an empty handle at `start()` makes the
1638
+ * real dispatch fall through, and this cannot see that.
1639
+ *
1640
+ * By default everything below the winner is `shadowed` without being asked,
1641
+ * which is what keeps this walk as cheap as the dispatch it replays. Pass
1642
+ * `{ evaluateShadowed: true }` to keep evaluating past the winner, so a
1643
+ * lower candidate that is ALSO ineligible or disabled says so — see
1644
+ * {@link ResolveAllOptions.evaluateShadowed}.
1645
+ */
1646
+ resolveAll(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): ResolvedCandidate[];
1647
+ /**
1648
+ * Synthesize an end-of-gesture for every in-flight ongoing handle.
1649
+ * Used by tool-switch cancellation (Q2 decision).
1650
+ */
1651
+ cancelAll(reason: 'commit' | 'cancel'): void;
1652
+ /**
1653
+ * Read-only view of currently in-flight ongoing handles, keyed by gestureId.
1654
+ * For debug/testing.
1655
+ */
1656
+ inFlight(): ReadonlyMap<string, OngoingHandle>;
1657
+ /**
1658
+ * CSS cursor for the gesture currently in flight, or `null` when nothing
1659
+ * is. Reads `Action.activeCursor` (falling back to `Action.cursor`) off the
1660
+ * action whose handle is open — the hover pump applies this instead of its
1661
+ * prediction once a gesture starts, which is how `grab` becomes `grabbing`.
1662
+ */
1663
+ inFlightCursor(): string | null;
1664
+ /**
1665
+ * Read-only iterator over currently in-flight `OngoingHandle` instances.
1666
+ *
1667
+ * Surface for the canvas's preview-ghost layer (`usePreviewGhostLayer`)
1668
+ * to walk each handle's `previewIds()` / `previewPose(id)` and render
1669
+ * dispatcher-driven gesture previews. Read-only by design:
1670
+ * external consumers must not mutate the in-flight map.
1671
+ */
1672
+ getInFlightHandles(): Iterable<OngoingHandle>;
1673
+ /**
1674
+ * Subscribe to in-flight state changes. The callback fires after every
1675
+ * mutation that affects what the preview-ghost / dispatcher-overlay
1676
+ * layers read — handle start, every `onMove` pump, end, cancel,
1677
+ * cancel-all. Consumers re-read `getInFlightHandles()` and re-render.
1678
+ *
1679
+ * Returns an unsubscribe function.
1680
+ */
1681
+ subscribe(fn: () => void): () => void;
1682
+ /**
1683
+ * Monotonic counter bumped on exactly the events {@link subscribe} fires
1684
+ * on. The snapshot half of the `useSyncExternalStore` contract: pair it
1685
+ * with `subscribe` to drive a render off in-flight gesture state without
1686
+ * a `useReducer` force-rerender.
1687
+ *
1688
+ * Starts at 0 and only ever increases. Two reads returning the same number
1689
+ * mean nothing pumped in between; it does **not** guarantee that a bump
1690
+ * changed anything observable (a pump that matched no binding still
1691
+ * counts — see `subscribe`).
1692
+ */
1693
+ getVersion(): number;
1694
+ /**
1695
+ * Snapshot of the currently active action, for surfaces (chrome-caps
1696
+ * visibility rules, debug HUDs) that need to react to "what action
1697
+ * is in flight right now."
1698
+ *
1699
+ * - `kind` — the `OngoingHandle.kind` reported by the in-flight
1700
+ * handle (e.g. `'marquee'`, `'move'`). `null` when no action is
1701
+ * in flight OR the handle didn't declare a kind.
1702
+ * - `id` — the dispatcher's internal `gestureId` (`pointer-1`,
1703
+ * `key-held-Space`, …) — the pointer/key channel the action rode
1704
+ * in on. `null` when no action is in flight.
1705
+ *
1706
+ * When multiple handles are in flight simultaneously (e.g. a key-held
1707
+ * action overlapping a pointer action), the most-recently-started
1708
+ * handle wins. This matches user intent: the latest interaction is
1709
+ * the one consumers care about.
1710
+ *
1711
+ * That rule used to be near-vacuous on the pointer side, because every
1712
+ * pointer shared one handle slot and two pointer drags could not coexist.
1713
+ * With per-pointer keying they can — but only via paths that bypass the
1714
+ * multi-pointer policy in `useGestureDispatcher` (which stops a second
1715
+ * finger from opening a drag while a pinch is live), such as a mouse and
1716
+ * a pen used together. Latest-start remains the right answer there.
1717
+ */
1718
+ getActiveAction(): {
1719
+ kind: string | null;
1720
+ id: string | null;
1721
+ };
1722
+ /**
1723
+ * Start an ongoing action driven by UI (not a gesture). Builds an
1724
+ * `InvocationCtx` with the given `deps` and `params`, calls
1725
+ * `action.invoker.start(ctx, { params })`, and registers the returned
1726
+ * handle in the in-flight map so `getInFlightHandles()` reports it —
1727
+ * enabling preview rendering via `SceneCanvas`.
1728
+ *
1729
+ * Returns `null` if `actionId` is unknown, the action's invoker is not
1730
+ * ongoing, or `start` returned an empty handle.
1731
+ *
1732
+ * If a UI-driven handle for the same `actionId` is already in flight,
1733
+ * it is committed (`end('commit')`) before the new one starts.
1734
+ */
1735
+ beginUiOngoing(actionId: string, deps: ActionDeps, params?: Record<string, unknown>): UiOngoingControl | null;
1736
+ }
1737
+ /** Build a dispatcher. `getAction` overrides how action ids are resolved;
1738
+ * by default the `DispatcherContext`'s registry is used. */
1739
+ declare function createDispatcher(opts?: {
1740
+ getAction?: (id: string) => Action | undefined;
1741
+ }): Dispatcher;
1742
+
1743
+ /**
1744
+ * @experimental
1745
+ * Partial override or full descriptor passed via `<SceneCanvas actions={...}>`.
1746
+ * `null` disables a default at this id.
1747
+ */
1748
+ type ActionEntry = null | Partial<Action> | Action;
1749
+ /**
1750
+ * @experimental
1751
+ * Shape of the `actions` prop on `<SceneCanvas>`. `null` disables all defaults.
1752
+ */
1753
+ type ActionsProp = null | Record<string, ActionEntry>;
1754
+ /**
1755
+ * @experimental
1756
+ * Imperative API exposed by `useActionsRegistry()`.
1757
+ */
1758
+ interface ActionsRegistry {
1759
+ /** Add an action and return its release. Registrants for one id stack,
1760
+ * newest live, so releasing yours uncovers whoever you displaced. Call it
1761
+ * from an effect and release it in that effect's cleanup — registering from
1762
+ * a render body pushes an entry per render and releases none. */
1763
+ register(action: Action): () => void;
1764
+ /** Drop every registrant of `id`. This is the "this action should not exist"
1765
+ * door, not a release — for that, call what `register` returned. */
1766
+ unregister(id: string): void;
1767
+ /** Declare `id` not for this scope: it stays registered, and every other
1768
+ * scope over the same store still resolves it, but here it lists as absent
1769
+ * and neither `trigger` nor `begin` will fire it. Returns a release; an
1770
+ * `<ActionsScope>` drops what it muted when it unmounts. This is the
1771
+ * "not for me" door — `unregister` is the "should not exist" one. */
1772
+ mute(id: string): () => void;
1773
+ list(): readonly Action[];
1774
+ /** Fire an immediate-invoker action by id. The optional `params` arg is
1775
+ * forwarded to `ImmediateInvoker.run` as its second argument — use it for
1776
+ * parametric actions (e.g. `trigger('tool.activate', { toolId: 'rect' })`).
1777
+ * Ongoing-invoker actions are not reachable from `trigger`. */
1778
+ trigger(id: string, params?: Record<string, unknown>): boolean;
1779
+ /**
1780
+ * Subscribe to registry mutations. The callback fires after any
1781
+ * `register`/`unregister` that changes the version. Returns an
1782
+ * unsubscribe function. Designed for `useSyncExternalStore`-driven
1783
+ * surfaces (e.g. `<ActionBar>` in `@weasel-js/ui`) that need to
1784
+ * re-render when the action set changes.
1785
+ */
1786
+ subscribe(listener: () => void): () => void;
1787
+ /**
1788
+ * Start an ongoing action driven by UI (color picker, opacity slider).
1789
+ * Returns a control object with `update(params)` and `end(reason)`.
1790
+ *
1791
+ * Returns `null` if no dispatcher is wired into this registry, the
1792
+ * action is unknown, or its invoker is not ongoing.
1793
+ *
1794
+ * See `Dispatcher.beginUiOngoing` for full semantics including
1795
+ * auto-commit when a prior UI handle for the same action is in flight.
1796
+ */
1797
+ begin(id: string, params?: Record<string, unknown>): UiOngoingControl | null;
1798
+ /** Wire a dispatcher into the registry so `begin()` can delegate to it.
1799
+ * Returns a release that clears the slot only while this dispatcher still
1800
+ * holds it: a canvas displaced by a later one must not take input away from
1801
+ * the canvas now on screen. Call with `null` to detach unconditionally. */
1802
+ setDispatcher(d: Dispatcher | null): () => void;
1803
+ /** Wire a `DepRegistry` into the registry so `trigger()` / `begin()` can
1804
+ * resolve action deps even when this provider is mounted ABOVE the dep
1805
+ * registry (e.g. a consumer's root `<ActionsProvider>` reused by
1806
+ * SceneCanvas's `ActionsProviderIfRoot`). Takes precedence over the dep
1807
+ * registry read from context at the provider's own level. Call with
1808
+ * `null` to detach unconditionally; the returned release clears the slot
1809
+ * only while this registry still holds it. */
1810
+ setDepRegistry(r: DepRegistry | null): () => void;
1811
+ }
1812
+ /** Validate that `id` is usable as an action id in the route grammar.
1813
+ * Rejects ids that start with a reserved sigil (would shadow future
1814
+ * grammar extensions) and ids that collide with phase keywords (would
1815
+ * parse as the bare-phase shorthand). Mirrors `defineTool`'s tool-id
1816
+ * validation. */
1817
+ declare function validateActionId(id: string): void;
1818
+ /** Reject any '&'-channel phase atom in an Action's defaultBinding —
1819
+ * actions have no owning tool, so '&' can't resolve. Tool-side bindings
1820
+ * (Tool.bindings) are the right place for '&' atoms. */
1821
+ declare function validateActionDefaultBinding(action: Action): void;
1822
+
1823
+ /** Canonical id of the consolidated tool-offhand action. One descriptor
1824
+ * serves every tool; the matched binding supplies `params.toolId` to select
1825
+ * which tool to engage while the key is held. */
1826
+ declare const TOOL_OFFHAND_ID = "tool.offhand";
1827
+ /** Per-tool spec consumed by `buildToolOffhandBindings`. The key is the
1828
+ * held-key trigger (e.g. ' ' for Space-for-hand). */
1829
+ interface ToolOffhandBindingSpec {
1830
+ toolId: string;
1831
+ key: string | string[];
1832
+ }
1833
+ /** `KeyboardEvent.key` for a declared `Eligibility.offhand` trigger. Only
1834
+ * `space` is exercised today; the modifier arms follow the DOM's names. */
1835
+ declare function offhandKeyFor(trigger: HotkeyTrigger): string;
1836
+ /** Build the per-tool `BoundGesture` list for the consolidated action's
1837
+ * `defaultBinding`. Each entry pairs a `key-held` spec with
1838
+ * `opts.params.toolId` so the invoker's `start` can pick out which tool
1839
+ * to engage. */
1840
+ declare function buildToolOffhandBindings(specs: readonly ToolOffhandBindingSpec[]): BoundGesture[];
1841
+ /** Build the consolidated `tool.offhand` action — "hold a key to engage a
1842
+ * secondary tool" (e.g. Space-for-hand). `bindings` enumerates one
1843
+ * `{ spec: keyHeld, opts: { params: { toolId } } }` per tool; the invoker
1844
+ * reads `params.toolId` from the matched binding, pushes the tool id onto
1845
+ * the active-tool context's hotkey stack on `start`, and pops it on `onEnd`.
1846
+ * Mirrors the parametric shape of `tool.activate`. */
1847
+ declare function makeToolOffhandAction(bindings: BoundGesture[]): Action;
1848
+
1849
+ /**
1850
+ * Named predicates for `defaultBinding.target.kindOf`. Each matches the
1851
+ * existing `kindOf` signature `(target: unknown, bodyTarget?: string) => boolean`
1852
+ * so they drop into action descriptors without a wrapper:
1853
+ *
1854
+ * target: { kindOf: isAnchor }
1855
+ *
1856
+ * Single source of truth for "what does this affordance kind mean" — adding
1857
+ * a new predicate here is what enables every action that needs the same
1858
+ * shape to read it. Drift prevention.
1859
+ */
1860
+ /** A predicate that resolves from `bodyTarget` alone. The exclusive-claim
1861
+ * filter reads `readsAffordance`: without it a body predicate looks like it
1862
+ * consults the hit, and survives a claim that should have barred it. */
1863
+ type BodyPredicate = ((target: unknown, bodyTarget?: string) => boolean) & {
1864
+ readsAffordance: false;
1865
+ };
1866
+ /** Matches any body hit — selected or unselected node body. */
1867
+ declare const isBody: BodyPredicate;
1868
+ /** Body hit specifically already in the selection. */
1869
+ declare const isSelectedBody: BodyPredicate;
1870
+ /** Body hit specifically NOT in the selection. */
1871
+ declare const isUnselectedBody: BodyPredicate;
1872
+ /** Empty-canvas hit — no node beneath cursor. */
1873
+ declare const isEmpty: BodyPredicate;
1874
+ /** Any corner resize handle — top-left/top-right/bottom-left/bottom-right. */
1875
+ declare const isResizeHandle: (target: unknown) => boolean;
1876
+ /** The rotate-around-center handle above the bounding box. */
1877
+ declare const isRotateHandle: (target: unknown) => boolean;
1878
+ /** Any path anchor (the on-curve dot). */
1879
+ declare const isAnchor: (target: unknown) => boolean;
1880
+ /** Either control handle (controlIn or controlOut). */
1881
+ declare const isControlHandle: (target: unknown) => boolean;
1882
+ /** Any of the three anchor-editing affordances (anchor + both controls). */
1883
+ declare const isAnchorOrControl: (target: unknown) => boolean;
1884
+
1885
+ /**
1886
+ * The DOM half of a drag: one pointer, held down, from press to release.
1887
+ *
1888
+ * Every pointerdown-to-pointerup lifecycle in the kit used to answer capture,
1889
+ * pointer identity, teardown and lost-pointer recovery for itself, and each
1890
+ * answered them differently. This is the one place those are decided; anything
1891
+ * above it — thresholds, coordinate spaces, what the drag *means* — belongs to
1892
+ * its caller.
1893
+ */
1894
+ /** Why a session ended without the pointer being released. */
1895
+ type PointerSessionCancelReason =
1896
+ /** The browser cancelled the pointer (touch interrupted, palm rejection). */
1897
+ 'pointercancel'
1898
+ /** The origin left the document mid-gesture, taking capture with it. */
1899
+ | 'lostcapture'
1900
+ /** `cancel()` — an unmount, a window blur, Escape, a consumer's own rule. */
1901
+ | 'aborted'
1902
+ /** A new press arrived on this pointer, so the tracked one had ended. */
1903
+ | 'superseded';
1904
+ interface PointerSessionCallbacks {
1905
+ /** Every move belonging to this pointer, while it is still held. */
1906
+ onMove?: (e: PointerEvent) => void;
1907
+ /** The pointer was released. Fires once, before teardown completes. */
1908
+ onEnd?: (e: PointerEvent) => void;
1909
+ /** The session ended without a release. Fires once. */
1910
+ onCancel?: (reason: PointerSessionCancelReason) => void;
1911
+ }
1912
+ interface PointerSessionOptions {
1913
+ /** Ask the origin element to capture the pointer. Default `true`. Capture
1914
+ * keeps hover and click off everything the drag passes over; the session
1915
+ * does not depend on it to keep receiving events. */
1916
+ capture?: boolean;
1917
+ }
1918
+ interface PointerSession {
1919
+ readonly pointerId: number;
1920
+ /** False once the session has ended, by any route. */
1921
+ readonly active: boolean;
1922
+ /** End the session now, reporting `'aborted'`. Idempotent. */
1923
+ cancel: () => void;
1924
+ }
1925
+ /**
1926
+ * Open a session for the pointer that `down` belongs to.
1927
+ *
1928
+ * Listens on the origin's document rather than on the element: a captured
1929
+ * element that is removed mid-drag stops receiving events, and every listener
1930
+ * hung on it goes with it. Three recovery rules close the gaps a plain
1931
+ * pointerup/pointercancel pair leaves — losing capture cancels only once the
1932
+ * origin has left the document, a move reporting no held button is read as
1933
+ * the release that never arrived, and a fresh press on the same pointer says
1934
+ * the tracked one had already ended.
1935
+ */
1936
+ declare function openPointerSession(origin: Element, down: PointerEvent | React.PointerEvent, cb: PointerSessionCallbacks, opts?: PointerSessionOptions): PointerSession;
1937
+
1938
+ /** What the registry knows at dispatch time. */
1939
+ interface EligibilityState {
1940
+ focusedId: string | null;
1941
+ heldTriggers: ReadonlySet<string>;
1942
+ /** Ids the host reports as hotkey-engaged. A declared `offhand` registers
1943
+ * the binding, but `tool.offhand`'s invoker still reports engagement by
1944
+ * pushing an id — this retires only if that contract changes. */
1945
+ engagedIds?: ReadonlySet<string>;
1946
+ /** Whether the active mode allows these capability tags. Omitted → allow. */
1947
+ allows?: (tags: readonly CapabilityTag[]) => boolean;
1948
+ }
1949
+ /**
1950
+ * The scope tier an entry's bindings are live at, or null when none are.
1951
+ * Ordered hotkey > active > ambient to match the dispatcher's own walk.
1952
+ */
1953
+ declare function liveScope(id: string, eligibility: Eligibility, state: EligibilityState): BindingScope | null;
1954
+
1955
+ /**
1956
+ * Every binding the entry set contributes right now, each tiered by the
1957
+ * entry's own declared eligibility. Entries with no live tier are omitted.
1958
+ *
1959
+ * Order is preserved: `matchSorted` sorts by specificity within a scope and
1960
+ * breaks ties by declaration order, so the caller's entry order decides which
1961
+ * of two same-specificity bindings in one tier wins.
1962
+ */
1963
+ declare function scopeBindings(entries: Iterable<Contribution>, state: EligibilityState): ScopedBinding[];
1964
+
1965
+ /**
1966
+ * Concatenate contribution bundles into one registry, preserving order —
1967
+ * entry order decides which of two same-specificity bindings in one scope
1968
+ * tier wins, so it is part of the result, not an accident of it.
1969
+ *
1970
+ * Throws on a duplicate id rather than dropping one: a feature silently
1971
+ * losing its bindings is the failure this registry exists to make loud.
1972
+ */
1973
+ declare function mergeContributions(...bundles: readonly Contribution[][]): Contribution[];
1974
+
1975
+ /**
1976
+ * What a tool declares.
1977
+ *
1978
+ * A tool is a declarative shell, not an event handler: it names the gestures
1979
+ * it responds to and, for each, the action to invoke. The dispatcher owns the
1980
+ * gesture, and the action owns the preview and the commit — so a tool
1981
+ * definition is mostly `bindings`, plus presentation and any actions the tool
1982
+ * itself introduces.
1983
+ */
1984
+ interface ToolDef<TScratch = void, TOverlay = unknown> {
1985
+ id: string;
1986
+ /** Capability tags for modality eligibility. Forwarded onto `Tool.capabilities`. */
1987
+ capabilities?: CapabilityTag[];
1988
+ /**
1989
+ * Actions this tool owns and needs registered while it is in the tools
1990
+ * registry — e.g. polygon's `polygon.adjustSides`, which its own bindings
1991
+ * reference by id.
1992
+ *
1993
+ * Declared here rather than registered by the hook with `useAction`,
1994
+ * because tool hooks run wherever the consumer calls them — for
1995
+ * `<SceneCanvas>` that is ABOVE `<ActionsProviderIfRoot>`, where
1996
+ * `useActionsRegistry()` returns null and `useAction` silently no-ops. The
1997
+ * result was a binding pointing at an action id nothing had registered, so
1998
+ * the gesture fell through to whatever matched next (polygon's
1999
+ * wheel/arrow-key side adjustment did nothing and `nudge.*` moved the
2000
+ * selection instead). `<ToolActionsMounter>` registers these from inside
2001
+ * the provider.
2002
+ */
2003
+ actions?: Action[];
2004
+ /** Hook name as exported from the kit barrel (e.g. `'useHandTool'`).
2005
+ * Set by built-in hooks for inspector / debugging. Consumer-authored
2006
+ * tools may set this to surface their hook name; omitted is fine.
2007
+ * Introspection-only — do not make this load-bearing in production.
2008
+ * Read off the def via `Tool.def` (the reflection escape hatch). */
2009
+ hookName?: string;
2010
+ presentation?: ToolPresentation<TScratch>;
2011
+ /** Optional caller-supplied activation key. Most built-in tools have their
2012
+ * activation key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`;
2013
+ * this field is for tools that want their activation key to be
2014
+ * configurable by the host (currently Lasso and Eyedropper). The dynamic
2015
+ * loop in `useKeybindings.ts` picks this up and appends a binding entry
2016
+ * to the consolidated `tool.activate` action (with `opts.params.toolId`
2017
+ * set so the invoker knows which tool to switch to). */
2018
+ keybinding?: ToolKeybinding;
2019
+ /** Held-key trigger: this tool engages while the key is down and
2020
+ * disengages on release. Carried onto `Tool.eligibility.offhand`, which
2021
+ * assembly reads to register the consolidated `tool.offhand` action — the
2022
+ * declaration is the wiring, with nothing for the host to do. */
2023
+ hotkey?: HotkeyTrigger;
2024
+ onActivate?: (ctx: ToolCtx<TScratch>) => void;
2025
+ onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
2026
+ cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
2027
+ /** Override the default scratch initializer. Default is `() => null`
2028
+ * cast to `TScratch`, which works for tools whose scratch is fresh
2029
+ * every gesture. Tools that need scratch identity to survive across
2030
+ * gesture boundaries (e.g. the pen tool's multi-click subpath state)
2031
+ * pass a stable-ref-returning thunk here. The factory forwards this
2032
+ * onto the returned `Tool.initScratch`. */
2033
+ initScratch?: () => TScratch;
2034
+ /** Declarative gesture bindings, forwarded onto `Tool.bindings`. The
2035
+ * gesture dispatcher consults these at active scope while this tool is the
2036
+ * active one, and at hotkey scope while it is held. This is the tool's
2037
+ * entire input surface — the `initial` / `engaged` phase tables that used
2038
+ * to sit beside it are gone, along with the second dispatcher that read
2039
+ * them. */
2040
+ bindings?: GestureBinding[];
2041
+ /** Optional overlay layer rendered while the tool occupies any slot
2042
+ * (active, hotkey, or ambient), surfaced on `Tool.overlay`.
2043
+ *
2044
+ * The layer's `draw` closure should read dynamic state through refs or
2045
+ * closures captured in the enclosing render scope, and gate on it there —
2046
+ * `if (!scratch.something) return []` — rather than expecting the kit to
2047
+ * swap layers as the gesture progresses.
2048
+ *
2049
+ * Pass an array to contribute several layers; they render in order. */
2050
+ overlay?: TOverlay | TOverlay[];
2051
+ /** Where `overlay` sits relative to the selection chrome. Defaults to
2052
+ * `'top'`, above everything. */
2053
+ overlayPosition?: OverlayPosition;
2054
+ }
2055
+ /** Viewport-tool spec. Once phase tables went away this stopped differing
2056
+ * from `ToolDef` in any structural way; `defineViewportTool` survives as the
2057
+ * authoring signal that a tool pans/zooms the view rather than the scene. */
2058
+ type ViewportToolDef<TScratch = void, TOverlay = unknown> = ToolDef<TScratch, TOverlay>;
2059
+
2060
+ /**
2061
+ * Build a `Tool<TScratch>` from a declarative `ToolDef<TScratch>`.
2062
+ *
2063
+ * This used to be a translator: `ToolDef` carried `initial` / `engaged` phase
2064
+ * tables of hit-keyed route handlers, and `defineTool` compiled them into the
2065
+ * imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the
2066
+ * tool-routing dispatcher called. Both ends of that are gone — tools declare
2067
+ * `bindings` and the gesture dispatcher routes them — so what remains is
2068
+ * identity plumbing plus the id check and the cursor resolver.
2069
+ *
2070
+ * It stays a function rather than becoming a spread because the id validation
2071
+ * and the `initScratch` / `cursor` defaults are worth applying uniformly, and
2072
+ * because `Tool.def` gives reflection a handle on the authored form.
2073
+ */
2074
+ declare function defineTool<TScratch = void, TOverlay = unknown>(def: ToolDef<TScratch, TOverlay>): Tool<TScratch, TOverlay>;
2075
+
2076
+ /**
2077
+ * Define a tool that acts on the viewport rather than the scene.
2078
+ *
2079
+ * This used to do real work: `ViewportPhaseDef` was a narrowed `PhaseDef`
2080
+ * (no click routes, drag restricted to the function form), and the factory
2081
+ * lifted it back to the permissive shape before handing it to `defineTool`.
2082
+ * With phase tables gone there is no shape left to narrow — a viewport tool
2083
+ * declares `bindings` like any other, pointing at `viewport.*` actions.
2084
+ *
2085
+ * It survives as an authoring signal. `defineViewportTool` at the top of a
2086
+ * hook says "this tool moves the camera, not the drawing", which is worth
2087
+ * more than the type gymnastics it replaced.
2088
+ */
2089
+ declare function defineViewportTool<TScratch = void, TOverlay = unknown>(def: ViewportToolDef<TScratch, TOverlay>): Tool<TScratch, TOverlay>;
2090
+
2091
+ /**
2092
+ * One row in the route registry — a single `GestureBinding` on one tool,
2093
+ * flattened into the route grammar's vocabulary so the inspector, the
2094
+ * conflict checker, and `describeRoute` can all read the same shape.
2095
+ *
2096
+ * Multiple rows can share (gesture, arg, target, modifiers) if different tools
2097
+ * declare them; consumers walk the list and group client-side.
2098
+ */
2099
+ interface RegistryEntry {
2100
+ toolId: string;
2101
+ /** Which phase the binding's `phase` spec restricts it to. `'any'` when it
2102
+ * declares none, declares `'*'`, or declares an atom list whose atoms
2103
+ * don't agree on one phase — such a binding fires in either phase, and
2104
+ * reporting it as `'initial'` made it collide with genuinely-initial
2105
+ * bindings in `findConflicts`' bucket key. */
2106
+ phase: 'initial' | 'engaged' | 'any';
2107
+ /** Structured v3 modifier requirements. Empty object = "no modifiers
2108
+ * held" (the strict default). */
2109
+ modifiers: ParsedModifiers;
2110
+ gesture: GestureName;
2111
+ /** Resolved arg value for arg-bearing gestures (wheel direction,
2112
+ * key name, multiTouchTap fingers). Undefined for gestures whose
2113
+ * descriptor has no `arg`. */
2114
+ arg: string | undefined;
2115
+ /** Every arg value this binding actually answers to, when the spec names
2116
+ * more than one — `key: ['Delete','Backspace']` answers to both, and
2117
+ * `matchKey` matches any member case-insensitively. `arg` collapses them
2118
+ * into one display token, which is right for a route string and wrong for
2119
+ * deciding whether two bindings collide. Absent when there is only one. */
2120
+ argAlternatives?: readonly string[];
2121
+ /** Target class for hit-testing gestures. `undefined` when the descriptor
2122
+ * has `hasTarget: false` or the spec declares no target;
2123
+ * {@link PREDICATE_TARGET} when the spec uses a `kindOf` predicate, which
2124
+ * the route grammar has no notation for. */
2125
+ target: string | undefined;
2126
+ /** The action this binding fires. */
2127
+ actionId: string;
2128
+ /** The `GestureSpec` this row was flattened from, by reference. Reflection
2129
+ * consumers that need something the grammar doesn't capture — the
2130
+ * specificity tuple, a `kindOf` predicate identity — read it here rather
2131
+ * than re-walking `Tool.bindings`. */
2132
+ spec: GestureSpec;
2133
+ }
2134
+ /**
2135
+ * Stand-in target for a `{ kindOf }` predicate spec.
2136
+ *
2137
+ * The route grammar can name target *classes* (`empty`, `selected-body`,
2138
+ * `kind:rect`) but not arbitrary predicates, so three distinct select-tool
2139
+ * bindings all render as this one token. Narrowing it would mean giving the
2140
+ * grammar a way to describe a function, which is a real design question and
2141
+ * not one this reflection layer should answer by inventing syntax.
2142
+ */
2143
+ declare const PREDICATE_TARGET = "predicate";
2144
+ /**
2145
+ * Flatten every tool's `bindings` into route-registry rows.
2146
+ *
2147
+ * This was `buildActionRegistry`, and it walked `ToolDef.initial` /
2148
+ * `.engaged` phase tables — the grammar that no longer exists. It was also
2149
+ * blind to `Tool.bindings` the entire time the two lived side by side, which
2150
+ * is why the inspector under-reported select by more than half. The rename
2151
+ * fixes a second thing: it never had anything to do with the Actions
2152
+ * Registry, and reading `buildActionRegistry` next to `ActionsRegistry`
2153
+ * suggested otherwise.
2154
+ */
2155
+ declare function buildRouteRegistry(tools: readonly Tool<unknown>[]): RegistryEntry[];
2156
+ /** The route-grammar gesture a `GestureSpec.kind` routes as, or `undefined`
2157
+ * for kinds the grammar has no name for. */
2158
+ declare function routeGestureForSpecKind(kind: GestureSpec['kind']): GestureName | undefined;
2159
+ /**
2160
+ * Every route string one `GestureSpec` declares, in route-grammar notation.
2161
+ *
2162
+ * One string per arg alternative — a spec bound to `['ArrowUp','ArrowDown']`
2163
+ * yields two routes, because each is separately conflictable and separately
2164
+ * readable. Returns `[]` for spec kinds the grammar can't name.
2165
+ *
2166
+ * This is the display counterpart to {@link buildRouteRegistry}: same
2167
+ * projection of a spec onto the grammar, rendered as text instead of as a
2168
+ * conflict-checkable row.
2169
+ */
2170
+ declare function routesForSpec(spec: GestureSpec): readonly string[];
2171
+
2172
+ /** Two or more tools declare the same exact (phase, gesture, arg, target,
2173
+ * modifiers) tuple — the dispatcher's slot precedence picks one
2174
+ * arbitrarily (well, deterministically by slot order, but the author
2175
+ * probably didn't intend the duplication). */
2176
+ interface Conflict {
2177
+ phase: 'initial' | 'engaged' | 'any';
2178
+ gesture: GestureName;
2179
+ arg: string | undefined;
2180
+ target: string | undefined;
2181
+ modifiers: ParsedModifiers;
2182
+ /** All tool ids that registered the same tuple. At least 2 by
2183
+ * construction. Order matches the input tools[] order. */
2184
+ toolIds: string[];
2185
+ }
2186
+ /** Detect exact-tuple overlaps across a tool registration set.
2187
+ *
2188
+ * Intentionally NOT flagged:
2189
+ * - Broad vs. narrow targets (e.g. an untargeted `click` alongside
2190
+ * `click` on `empty`) — the dispatcher's specificity ordering resolves
2191
+ * those cleanly, and the broad one is usually the intended fallback.
2192
+ * - Different modifier requirements on the same target — they fire on
2193
+ * different inputs.
2194
+ * - A binding whose action declines via `enabled()` so a lower-priority
2195
+ * one can take the gesture. Detecting that intent would mean evaluating
2196
+ * the action; consumers can suppress known-intentional compositions in
2197
+ * their UI layer.
2198
+ *
2199
+ * - Two `{ kindOf }` predicate targets. The route grammar renders every
2200
+ * predicate as the single token {@link PREDICATE_TARGET}, so bucketing on
2201
+ * the rendered target alone reported select's `resize` / `rotate` / `move`
2202
+ * drags — three genuinely different predicates — as a three-way conflict.
2203
+ * Predicate entries bucket by function identity instead, which means two
2204
+ * *separately written but equivalent* predicates go unflagged. That's the
2205
+ * right way to be wrong here: this check has to be silent when nothing is
2206
+ * wrong or nobody will keep it on.
2207
+ *
2208
+ * Note that two bindings sharing a tuple on the SAME tool are now possible
2209
+ * (bindings are an array, where phase tables were objects with unique
2210
+ * keys) — so a conflict may name one tool twice.
2211
+ *
2212
+ * This is the raw same-tuple detector. Feeding it a whole tool *registry*
2213
+ * over-reports, because registry tools take turns in the active slot and
2214
+ * can't collide with each other — see {@link findScopedConflicts}.
2215
+ */
2216
+ declare function findConflicts(tools: readonly Tool<unknown>[]): Conflict[];
2217
+ /**
2218
+ * The tool scopes as the dispatcher sees them — which is what decides whether
2219
+ * two same-tuple bindings can actually collide.
2220
+ */
2221
+ interface ToolScopes {
2222
+ /** Tools eligible for the active slot, keyed by id or as a flat list. */
2223
+ registry: readonly Tool<unknown>[] | Readonly<Record<string, Tool<unknown>>>;
2224
+ /** Always-on tools. Every one of these is live at once. */
2225
+ ambient?: readonly Tool<unknown>[];
2226
+ /** Registered actions. Their `defaultBinding`s assemble at ambient scope
2227
+ * (hotkey scope when `Action.scope` says so), alongside the tools above. */
2228
+ actions?: readonly Action[];
2229
+ }
2230
+ /**
2231
+ * Detect reachable route conflicts in an assembled tool set and report each one.
2232
+ *
2233
+ * This is the wiring `findConflicts` spent its first life without: the kit
2234
+ * could detect the one class of genuine routing ambiguity it has and never
2235
+ * looked. Call it once wherever a tool set is assembled (`useTools` does),
2236
+ * behind a `process.env.NODE_ENV !== 'production'` guard.
2237
+ *
2238
+ * **Warn, never throw.** A conflict between a consumer's tool and a kit tool
2239
+ * is a design question — sometimes deliberate, since the loser can still take
2240
+ * the gesture by declining through `enabled()` — and exploding in a running
2241
+ * app is the wrong way to raise it. A conflict between two *kit* tools is
2242
+ * always a bug, but the place to fail on that is the kit's own test suite
2243
+ * (`canvas/SceneCanvas.routeConflicts.test.tsx`), not a consumer's console.
2244
+ *
2245
+ * @returns The conflicts found, so callers can dedupe repeat reports.
2246
+ */
2247
+ declare function reportRouteConflicts(scopes: ToolScopes, warn?: (message: string) => void): Conflict[];
2248
+
2249
+ /**
2250
+ * Pure keyboard-binding helpers shared between the tool dispatcher
2251
+ * (`useKeybindings`), the gesture dispatcher (`useGestureDispatcher`), and
2252
+ * individual Tool declarations.
2253
+ *
2254
+ * Extracted from the deleted singular `useKeybinding` hook so the remaining
2255
+ * consumers don't depend on its disappearance.
2256
+ */
2257
+ /**
2258
+ * @internal — runtime shape consumed by `matchesKeyBinding`. Public tool authors
2259
+ * should use `ToolKeybinding` from `@weasel-js/core` for declaring
2260
+ * configurable activation keys.
2261
+ */
2262
+ interface KeyBinding {
2263
+ /**
2264
+ * Key or list of keys to match. Compared case-insensitively against
2265
+ * `event.key`, so `'a'` matches both `'a'` and `'A'`.
2266
+ */
2267
+ key: string | readonly string[];
2268
+ /**
2269
+ * Require Cmd (mac) or Ctrl (others). When `true`, exactly one of
2270
+ * `metaKey`/`ctrlKey` must be held. Default `false` (both forbidden).
2271
+ */
2272
+ mod?: boolean;
2273
+ /** Require Alt. Default `false` (forbidden). */
2274
+ alt?: boolean;
2275
+ /**
2276
+ * Shift policy. `undefined`/`false` forbids shift, `true` requires
2277
+ * shift, `'optional'` allows either. Default `undefined`.
2278
+ */
2279
+ shift?: boolean | 'optional';
2280
+ /** Skip when focus is in an editable element. Default `true`. */
2281
+ skipInEditable?: boolean;
2282
+ /** When `false`, the listener is not attached. Default `true`. */
2283
+ enabled?: boolean;
2284
+ /** Call `preventDefault` before the handler. Default `true`. */
2285
+ preventDefault?: boolean;
2286
+ }
2287
+ /** Returns true when the target is an input, textarea, or contenteditable element. */
2288
+ declare function isEditableTarget(target: EventTarget | null): boolean;
2289
+ /**
2290
+ * Pure matcher for `KeyBinding` against a `KeyboardEvent`. Only checks key +
2291
+ * modifier policy — does NOT apply the editable-target skip or
2292
+ * `preventDefault`. Shared by the tool dispatcher (`useKeybindings`) and the
2293
+ * gesture dispatcher so the rules can't drift.
2294
+ */
2295
+ declare function matchesKeyBinding(e: KeyboardEvent, b: KeyBinding): boolean;
2296
+
2297
+ /**
2298
+ * Typed keys for the gesture/behavior scratch store.
2299
+ *
2300
+ * Gestures expose `ctx.scratch: Record<string, unknown>` as a per-gesture
2301
+ * mutable bag that behaviors use to stash state across `onDown` / `onMove`
2302
+ * / `onEnd` calls. The store is `unknown`-valued because behaviors are
2303
+ * tool-agnostic — a single behavior may run under different tools with
2304
+ * different scratch shapes. Without a typing convention every read site
2305
+ * has to `(ctx.scratch as <some shape>)[KEY]`, and a writer changing the
2306
+ * shape can silently break readers with no compile-time signal.
2307
+ *
2308
+ * `ScratchKey<T>` is a phantom-typed string that carries its payload type
2309
+ * via a branded interface. `getScratch` and `setScratch` use the brand to
2310
+ * narrow / constrain at the typing layer; at runtime the key is just its
2311
+ * underlying string and the store remains a plain `Record`.
2312
+ *
2313
+ * @example
2314
+ * ```ts
2315
+ * // Module-level (or behavior-level) constant, shared by writer + reader:
2316
+ * const LASSO_VERTICES = scratchKey<readonly { x: number; y: number }[]>('lasso.vertices');
2317
+ *
2318
+ * // Writer (a tool's onMove):
2319
+ * setScratch(ctx.scratch, LASSO_VERTICES, vertices);
2320
+ *
2321
+ * // Reader (a behavior's onEnd):
2322
+ * const vertices = getScratch(ctx.scratch, LASSO_VERTICES) ?? [];
2323
+ * // ^? readonly { x: number; y: number }[] | undefined
2324
+ * ```
2325
+ */
2326
+ /** Phantom-typed key for a scratch slot. The `__scratchKeyPayload` phantom
2327
+ * field carries the value type without occupying any runtime memory. */
2328
+ interface ScratchKey<T> {
2329
+ readonly name: string;
2330
+ /** Phantom — never read at runtime. Only present so TypeScript can
2331
+ * recover `T` at use sites. */
2332
+ readonly __scratchKeyPayload?: (_: T) => T;
2333
+ }
2334
+ /** Compatible scratch store shape. The kit's gesture context's `scratch`
2335
+ * field already satisfies this; consumers don't need to construct one. */
2336
+ type ScratchStore = Record<string, unknown>;
2337
+ /**
2338
+ * Make a typed scratch key. Use module-level constants so the same key
2339
+ * is shared by writer and reader.
2340
+ *
2341
+ * Namespace the name by behavior or feature to avoid collisions
2342
+ * (`'behavior.field'` is a good convention). Two keys with the same
2343
+ * `name` refer to the same slot at runtime — the type parameter is a
2344
+ * compile-time contract, not a uniqueness guarantee.
2345
+ */
2346
+ declare function scratchKey<T>(name: string): ScratchKey<T>;
2347
+ /** Read a typed slot from the scratch store. Returns `undefined` if
2348
+ * nothing has been written under the key. */
2349
+ declare function getScratch<T>(store: ScratchStore, key: ScratchKey<T>): T | undefined;
2350
+ /** Write a value to a typed slot. Overwrites whatever was there. */
2351
+ declare function setScratch<T>(store: ScratchStore, key: ScratchKey<T>, value: T): void;
2352
+ /** Remove a slot from the scratch store. Returns `true` if it was set,
2353
+ * `false` if it wasn't. */
2354
+ declare function deleteScratch<T>(store: ScratchStore, key: ScratchKey<T>): boolean;
2355
+
2356
+ /**
2357
+ * IngestItem materialization — turns a live `DataTransfer` (drop) or
2358
+ * clipboard `DataTransfer` (paste) into fully-owned `IngestItem[]`.
2359
+ *
2360
+ * `DataTransfer` items are only readable while the originating DOM event is
2361
+ * on the stack, so BOTH functions must be called synchronously from the event
2362
+ * handler. `itemsFromDataTransfer` kicks off every `getAsString` read
2363
+ * immediately and resolves once all strings are collected;
2364
+ * `itemsFromClipboardData` is fully synchronous (`getData` + `files`).
2365
+ *
2366
+ * MIME strings are normalized to bare lowercase `type/subtype` (parameters
2367
+ * like `;charset=utf-8` stripped) — downstream exact matching (binding
2368
+ * `types`, content-handler `match`) assumes the bare form.
2369
+ *
2370
+ * Output shape: **files first (source order), then strings (item order)**.
2371
+ * For paste (`itemsFromClipboardData`) string flavors are emitted in
2372
+ * `INGEST_STRING_MIMES` order: text/plain, text/html, text/uri-list.
2373
+ */
2374
+
2375
+ /** The string flavors the kit normalizes. Everything else (browser-internal
2376
+ * custom flavors, etc.) is dropped at materialization. */
2377
+ declare const INGEST_STRING_MIMES: ReadonlySet<string>;
2378
+ /** Wrap raw `File`s (file picker, `CanvasExtensionApi.ingest`) as
2379
+ * `IngestItem`s, normalizing each MIME the same way the drop/paste paths
2380
+ * do. Synchronous — `File` objects are already fully owned. */
2381
+ declare function itemsFromFiles(files: File[]): IngestItem[];
2382
+ /** Materialize a drop's `DataTransfer`. MUST be called during the `drop`
2383
+ * event dispatch (see module doc). */
2384
+ declare function itemsFromDataTransfer(dt: DataTransfer): Promise<IngestItem[]>;
2385
+ /** Materialize a paste's `clipboardData`. Fully synchronous. */
2386
+ declare function itemsFromClipboardData(cd: DataTransfer): IngestItem[];
2387
+
2388
+ /**
2389
+ * Convert client coords to element CSS-pixel coords (relative to the element's
2390
+ * top-left). Apps drawing in CSS-pixel space feed the result directly into
2391
+ * world math when pan/zoom is identity. With pan/zoom, compose this with
2392
+ * your inverse-viewport transform.
2393
+ */
2394
+ declare function clientToCanvas(canvas: HTMLElement, clientX: number, clientY: number): [number, number];
2395
+ /**
2396
+ * Rect-taking variant of `clientToCanvas` for call sites that already hold a
2397
+ * `DOMRect` (or any `{ left, top }` object) and don't have a canvas element
2398
+ * in scope.
2399
+ */
2400
+ declare function clientToCanvasRect(rect: {
2401
+ left: number;
2402
+ top: number;
2403
+ }, clientX: number, clientY: number): [number, number];
2404
+
2405
+ /** Conditional `console.debug` — no-op unless `namespace` (or `*`) is enabled.
2406
+ * Each call is prefixed with `[namespace]` so the source is searchable. */
2407
+ declare function dlog(namespace: string, ...args: unknown[]): void;
2408
+
2409
+ /**
2410
+ * @weasel-js/routing — binding-to-action routing.
2411
+ *
2412
+ * The pure half: no React, no DOM. The React seam that pumps browser events
2413
+ * into the dispatcher, and the providers that feed it, live behind
2414
+ * `@weasel-js/routing/react`.
2415
+ */
2416
+
2417
+ /**
2418
+ * The dependency schema — the map of dep name → value type that
2419
+ * `Action.requires`, `DepRegistry.register`/`get` and `useDepSource` are all
2420
+ * keyed on.
2421
+ *
2422
+ * It is declared **empty** here on purpose. Routing knows that an action names
2423
+ * its dependencies and that the registry resolves them at invocation time; it
2424
+ * does not know what a selection, a scene or a viewport is. Whoever supplies
2425
+ * those merges them in:
2426
+ *
2427
+ * ```ts
2428
+ * declare module '@weasel-js/routing' {
2429
+ * interface DepSchema { selection: SelectionApi }
2430
+ * }
2431
+ * ```
2432
+ *
2433
+ * `@weasel-js/core` merges the kit's own 24 deps this way and re-exports
2434
+ * `DepSchema`, so a consumer adds its own against `'@weasel-js/core'` and
2435
+ * never has to know this module exists. The merge carries through the
2436
+ * re-export in both directions — `scripts/smoke-consumer-bundle.mjs` asserts
2437
+ * it against the published `.d.ts`, which is where it once broke: a
2438
+ * cross-module augmentation stops merging when `rollup-plugin-dts` flattens
2439
+ * both files into one chunk.
2440
+ */
2441
+ interface DepSchema {
2442
+ }
2443
+ /** Every dep name declared by anything that has merged into {@link DepSchema}. */
2444
+ type DepName = keyof DepSchema;
2445
+
2446
+ export { INGEST_STRING_MIMES as $, type ActionsRegistry as A, type BindingOpts as B, type Contribution as C, type Dispatcher as D, type Condition as E, type Conflict as F, type ContributionChrome as G, type ContributionRouting as H, DEFAULT_ALLOWED_CAPABILITIES as I, DEFAULT_DEVICE_PROFILE as J, type DebugSink as K, type DepName as L, type DetectedDeviceFacts as M, type DeviceProfile as N, type OverlayPosition as O, type DispatchLogEntry as P, type DispatcherContext as Q, type RuleCtx as R, type ScopedBinding as S, type Tool as T, type DragSample as U, type Eligibility as V, type EligibilityState as W, type GestureBinding as X, type HandleKind as Y, type HitShape as Z, type HotkeyTrigger as _, type AffordanceHit as a, isAnchorOrControl as a$, type ImmediateInvoker as a0, type InvocationCtx as a1, type Invoker as a2, type KeyBinding as a3, type MatchResult as a4, type ModeSwitchLogEntry as a5, type ModifierState as a6, NEVER as a7, type NodeId as a8, type OngoingHandle as a9, type ToolModifiers as aA, type ToolOffhandBindingSpec as aB, type ToolPresentation as aC, type ToolSlot as aD, type TraceLogEntry as aE, type UiOngoingControl as aF, type View as aG, type ViewportToolDef as aH, actionBindings as aI, buildDepsFromRequires as aJ, buildRouteRegistry as aK, buildRuleCtx as aL, buildToolOffhandBindings as aM, clientToCanvas as aN, clientToCanvasRect as aO, createDispatcher as aP, defineTool as aQ, defineViewportTool as aR, deleteScratch as aS, describeRule as aT, dlog as aU, evaluate as aV, evaluateEnabled as aW, filterEligible as aX, findConflicts as aY, getScratch as aZ, isAnchor as a_, type OngoingInvoker as aa, type OngoingOverlay as ab, type OverlayRole as ac, PREDICATE_TARGET as ad, type Point2 as ae, type PointerSession as af, type PointerSessionCallbacks as ag, type PointerSessionCancelReason as ah, type PointerSessionOptions as ai, type RegistryEntry as aj, type ResizeAnchor as ak, type ResolveAllOptions as al, type ResolveOnlyResult as am, type ResolvedCandidate as an, type Rule as ao, type ScratchKey as ap, type ScratchStore as aq, type SelectionApi as ar, type SelectionExtendKey as as, type SelectionMode as at, type Selector as au, TOOL_OFFHAND_ID as av, type ToolBounds as aw, type ToolCtx as ax, type ToolDef as ay, type ToolKeybinding as az, type DepSchema as b, isBody as b0, isControlHandle as b1, isEditableTarget as b2, isEmpty as b3, isResizeHandle as b4, isRotateHandle as b5, isSelectedBody as b6, isUnselectedBody as b7, itemsFromClipboardData as b8, itemsFromDataTransfer as b9, itemsFromFiles as ba, liveScope as bb, makeToolOffhandAction as bc, matchBest as bd, matchSorted as be, matchesKeyBinding as bf, mergeContributions as bg, offhandKeyFor as bh, openPointerSession as bi, pointerGestureId as bj, recordModeSwitch as bk, reportRouteConflicts as bl, resolveDeviceProfile as bm, resolveParams as bn, routeGestureForSpecKind as bo, routesForSpec as bp, scopeBindings as bq, scratchKey as br, setScratch as bs, specificity as bt, targetConsultsAffordance as bu, validateActionDefaultBinding as bv, validateActionId as bw, type Action as c, type AnyToolOf as d, type DepRegistry as e, DepRegistryProvider as f, useDepSource as g, useOptionalDepRegistry as h, ALWAYS as i, type ActionDeps as j, ActionDisabledReason as k, type ActionDispatch as l, type ActionEnabledResult as m, type ActionEntry as n, type ActionPresentation as o, type ActionSource as p, type ActionsProp as q, type AnyTool as r, type BindingScope as s, type BindingSource as t, useDepRegistry as u, type BodyPredicate as v, type BoundGesture as w, type Bounds as x, type BuildRuleCtxArgs as y, COARSE_TARGET_SCALE as z };