@weasel-js/gestures 1.11.0 → 1.11.1

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.
package/dist/index.d.ts CHANGED
@@ -81,10 +81,13 @@ type RoutePhase = 'initial' | 'engaged';
81
81
  * argSlot = '(' argValue ')' -- whitespace inside parens is significant
82
82
  * targetSlot = '=>' WS targetValue -- omitted slot defaults to '*' for hasTarget
83
83
  * modSlot = modAtom (WS modAtom)*
84
- * modAtom = sigil modName
84
+ * modAtom = sigil (modName | 'held')
85
85
  * sigil = '+' | '?' -- ! @ # $ % ^ & * reserved as id-prefix
86
86
  * modName = 'mod' | 'shift' | 'alt' | 'ctrl' | 'meta'
87
87
  *
88
+ * `held` is a pressed button, read as a modifier is: `?held` a button may be
89
+ * held, `+held` one must be, absent none may. Only `hover` takes it.
90
+ *
88
91
  * The truly-loose form (any channel, any phase) is `[*:*]`.
89
92
  *
90
93
  * Examples:
@@ -152,6 +155,9 @@ interface ParsedRoute {
152
155
  target: string | undefined;
153
156
  /** Structured modifier requirements; empty map = "no modifiers held". */
154
157
  modifiers: ParsedModifiers;
158
+ /** Whether a pressed button may or must be held. Absent, none may. Only on
159
+ * `hover`. */
160
+ held?: ModRequirement;
155
161
  }
156
162
  /** Parse a route string into its slots, filling in the shorthand defaults the
157
163
  * grammar defines (bare phases mean channel `&`; an omitted arg or target
@@ -338,7 +344,8 @@ interface PointerDownSpec {
338
344
  }
339
345
  /**
340
346
  * The pointer moving over the surface with no button pressed. Fires on every
341
- * such move; a held pointer's moves belong to its drag and fire nothing here.
347
+ * such move; a held pointer's moves belong to its drag and fire nothing here,
348
+ * unless `held` says otherwise.
342
349
  *
343
350
  * With `motion: 'leave'` it fires instead when the pointer passes out of what
344
351
  * the binding targets: onto something its `target` does not match, or off the
@@ -349,11 +356,16 @@ interface PointerDownSpec {
349
356
  * runs, not only the best one, and the move still reaches the hover-cursor
350
357
  * pump. There is no press to own, so bind an immediate action, not an ongoing
351
358
  * one. Its params carry `worldX` / `worldY`, `clientX` / `clientY`,
352
- * `affordance`, `bodyTarget`, and `mods`.
359
+ * `affordance`, `bodyTarget`, `mods`, and `held`.
353
360
  */
354
361
  interface HoverSpec {
355
362
  kind: 'hover';
356
363
  motion?: 'move' | 'leave';
364
+ /** Whether a pressed button may (`'optional'`) or must (`true`) be held, as
365
+ * `mods` says of a modifier; omitted, a held pointer's moves are not hovers.
366
+ * Such a binding still claims nothing, so the drag keeps its moves. Leaving
367
+ * the surface is a leave whether or not a button is held. */
368
+ held?: true | 'optional';
357
369
  target?: TargetSpec;
358
370
  mods?: ModSpec;
359
371
  phase?: PhaseSpec;
@@ -410,10 +422,15 @@ interface DropSpec {
410
422
  * its moves update it, the release commits it, and leaving the surface or
411
423
  * canceling the press cancels it. An immediate action fires once, on the
412
424
  * release.
425
+ *
426
+ * `target` is matched against what is under the carry. With one, an ongoing
427
+ * action starts on the first step that is over its target, as the carry comes
428
+ * over or as it moves on, and an immediate action fires only on a release over it.
413
429
  */
414
430
  interface CarrySpec {
415
431
  kind: 'carry';
416
432
  types?: string[];
433
+ target?: TargetSpec;
417
434
  mods?: ModSpec;
418
435
  phase?: PhaseSpec;
419
436
  }
@@ -795,6 +812,8 @@ interface HoverEvent extends EventModifiers {
795
812
  from?: HoverWhere;
796
813
  /** The pointer has left the surface. Where it is and what it is over are its last move's. */
797
814
  off?: boolean;
815
+ /** A button was held through this move. Never set on `off`. */
816
+ held?: boolean;
798
817
  /** World-space position of the pointer. */
799
818
  x: number;
800
819
  y: number;
@@ -899,6 +918,10 @@ interface CarryEvent extends EventModifiers {
899
918
  y: number;
900
919
  clientX: number;
901
920
  clientY: number;
921
+ /** What is under the carry, classified as a press there would be. */
922
+ affordance?: unknown;
923
+ bodyTarget?: BodyTarget;
924
+ bodyKind?: BodyKind;
902
925
  }
903
926
  /** External content pasted from the system clipboard. Carries no point. */
904
927
  interface PasteEvent extends EventModifiers {
@@ -1168,6 +1191,14 @@ declare function formatShortcut(b: ShortcutInput | undefined, opts?: FormatShort
1168
1191
  */
1169
1192
  declare function keySpecsFromShortcut(shortcut: ShortcutInput | undefined, opts?: KeySpecsFromModsOptions): readonly KeyLegend[] | undefined;
1170
1193
 
1194
+ /**
1195
+ * The legend of the physical key a `KeyboardEvent.code` names (`KeyA`, `ShiftLeft`, `ArrowUp`), as
1196
+ * {@link keySpecFromKey} prints that key on the platform: `MetaLeft` is `⌘` on macOS and `⊞` on
1197
+ * Windows. Letters and punctuation print as a US keyboard prints them. `opts.labels` answers by code
1198
+ * first, then as it does for {@link keySpecFromKey} and {@link keySpecsFromMods}.
1199
+ */
1200
+ declare function keySpecFromCode(code: string, opts?: KeySpecsFromModsOptions): KeyLegend;
1201
+
1171
1202
  /**
1172
1203
  * The nearest element `from` sits inside that can host a portalled overlay,
1173
1204
  * or `null` when there is none and the body is the only answer.
@@ -1356,6 +1387,13 @@ interface PointerSample extends EventModifiers, PointerSampleStylus {
1356
1387
  clientY: number;
1357
1388
  /** What the originating event was aimed at. */
1358
1389
  target?: unknown;
1390
+ /**
1391
+ * When the input happened, on the recognizer clock's timeline: a DOM event's
1392
+ * `timeStamp` beside `performance.now()`. The windows between two inputs are
1393
+ * measured on it, so a long task that delays an event's handling does not
1394
+ * widen them. Read as the clock's `now()` when absent, 0, or ahead of it.
1395
+ */
1396
+ timeStamp?: number;
1359
1397
  }
1360
1398
  /** A pointer going down, with what the host found under it. */
1361
1399
  interface PressSample extends PointerSample {
@@ -1485,4 +1523,4 @@ declare function activatesFocusedControl(e: KeyboardEvent): boolean;
1485
1523
  */
1486
1524
  declare function focusedControlOwnsKey(e: KeyboardEvent): boolean;
1487
1525
 
1488
- export { ACTIVE_SIGILS, type BodyClassification, type BodyKind, type BodyTarget, CONTROL_SELECTOR, type CarryEvent, type CarrySpec, type ChannelRef, type ClaimableGesture, type ClickEvent, type ClickMemory, type ClickObserver, type ClickSpec, type ContextMenuEvent, type ContextMenuSpec, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, type DescribeRouteOptions, type DoubleClickBody, type DoubleClickEvent, type DoubleClickSpec, type DragSpec, type DropEvent, type DropSpec, type EventModifiers, type FixedSeat, type FormatShortcutOptions, GESTURE_DESCRIPTORS, type GestureArgSpec, type GestureDescriptor, type GestureName, type GestureSpec, type HeldPointers, type HeldPress, type HoverEvent, type HoverSpec, type HoverWhere, type IngestItem, type InputEvent, type KeyEvent, type KeyHeldEvent, type KeyHeldSpec, type KeyLabels, type KeyLegend, type KeySpec, type KeySpecsFromModsOptions, LONG_PRESS_MS, type LegendStyle, type LogicalMod, type LogicalModSpec, type LongPressEvent, type LongPressSpec, MODIFIER_LABELS, MOD_NAME_SET, type ModRequirement, type ModSpec, type ModifierCombo, type ModifierKey, type ModifiersEvent, type MultiTouchEvent, type MultiTouchGeometry, type MultiTouchSpec, type MultiTouchTapEvent, type MultiTouchTapSpec, type OptionalMod, type ParsedKeyRoute, type ParsedModifiers, type ParsedRoute, type PasteEvent, type PasteSpec, type PhaseAtom, type PhaseContext, type PhaseSpec, type PinchEvent, type PinchSpec, type Platform, type Point, type PointerCancelEvent, type PointerDownEvent, type PointerDownSpec, type PointerIdentity, type PointerMoveEvent, type PointerRecognizer, type PointerRecognizerOptions, type PointerSample, type PointerSampleStylus, type PointerUpEvent, type PressFacts, type PressSample, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, type RecognizerClock, type RecognizerObserver, type RecognizerSink, type ReleaseFacts, type ReleaseOutcome, type RouteDescriptionPart, type RouteFieldName, type RoutePhase, type RouteTermLabel, type ShortcutInput, TAP_THRESHOLD_PX, type TargetPredicate, type TargetSpec, type TargetSpecForm, UNKNOWN_MIME, VALID_MOD_NAMES, type WheelEvent, type WheelSpec, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, detectPlatform, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, formatShortcut, formatShortcutParts, getGestureDescriptor, heldPointers, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, keySpecFromKey, keySpecsFromMods, keySpecsFromShortcut, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
1526
+ export { ACTIVE_SIGILS, type BodyClassification, type BodyKind, type BodyTarget, CONTROL_SELECTOR, type CarryEvent, type CarrySpec, type ChannelRef, type ClaimableGesture, type ClickEvent, type ClickMemory, type ClickObserver, type ClickSpec, type ContextMenuEvent, type ContextMenuSpec, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, type DescribeRouteOptions, type DoubleClickBody, type DoubleClickEvent, type DoubleClickSpec, type DragSpec, type DropEvent, type DropSpec, type EventModifiers, type FixedSeat, type FormatShortcutOptions, GESTURE_DESCRIPTORS, type GestureArgSpec, type GestureDescriptor, type GestureName, type GestureSpec, type HeldPointers, type HeldPress, type HoverEvent, type HoverSpec, type HoverWhere, type IngestItem, type InputEvent, type KeyEvent, type KeyHeldEvent, type KeyHeldSpec, type KeyLabels, type KeyLegend, type KeySpec, type KeySpecsFromModsOptions, LONG_PRESS_MS, type LegendStyle, type LogicalMod, type LogicalModSpec, type LongPressEvent, type LongPressSpec, MODIFIER_LABELS, MOD_NAME_SET, type ModRequirement, type ModSpec, type ModifierCombo, type ModifierKey, type ModifiersEvent, type MultiTouchEvent, type MultiTouchGeometry, type MultiTouchSpec, type MultiTouchTapEvent, type MultiTouchTapSpec, type OptionalMod, type ParsedKeyRoute, type ParsedModifiers, type ParsedRoute, type PasteEvent, type PasteSpec, type PhaseAtom, type PhaseContext, type PhaseSpec, type PinchEvent, type PinchSpec, type Platform, type Point, type PointerCancelEvent, type PointerDownEvent, type PointerDownSpec, type PointerIdentity, type PointerMoveEvent, type PointerRecognizer, type PointerRecognizerOptions, type PointerSample, type PointerSampleStylus, type PointerUpEvent, type PressFacts, type PressSample, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, type RecognizerClock, type RecognizerObserver, type RecognizerSink, type ReleaseFacts, type ReleaseOutcome, type RouteDescriptionPart, type RouteFieldName, type RoutePhase, type RouteTermLabel, type ShortcutInput, TAP_THRESHOLD_PX, type TargetPredicate, type TargetSpec, type TargetSpecForm, UNKNOWN_MIME, VALID_MOD_NAMES, type WheelEvent, type WheelSpec, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, detectPlatform, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, formatShortcut, formatShortcutParts, getGestureDescriptor, heldPointers, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, keySpecFromCode, keySpecFromKey, keySpecsFromMods, keySpecsFromShortcut, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ var GESTURE_DESCRIPTORS = [
18
18
  // matches any payload, which is what an omitted arg slot means for a
19
19
  // `'free'` arg.
20
20
  { name: "drop", hasTarget: false, arg: { name: "types", values: "free" } },
21
- { name: "carry", hasTarget: false, arg: { name: "types", values: "free" } },
21
+ { name: "carry", hasTarget: true, arg: { name: "types", values: "free" } },
22
22
  { name: "paste", hasTarget: false, arg: { name: "types", values: "free" } }
23
23
  ];
24
24
  var BY_NAME = new Map(
@@ -107,6 +107,7 @@ function parseRoute(input) {
107
107
  target = "*";
108
108
  }
109
109
  const modifiers = {};
110
+ let held;
110
111
  while (rest.length > 0) {
111
112
  const ch = rest[0];
112
113
  if (RESERVED_SIGILS.has(ch)) {
@@ -120,6 +121,13 @@ function parseRoute(input) {
120
121
  const nameMatch = /^([a-z]+)/.exec(rest);
121
122
  if (!nameMatch) throw new Error(`invalid route (sigil "${sigil}" without modifier name): ${input}`);
122
123
  const name = nameMatch[1];
124
+ if (name === HELD) {
125
+ if (gestureName !== "hover") throw new Error(`invalid route (${gestureName} takes no "${HELD}"): ${input}`);
126
+ if (held !== void 0) throw new Error(`invalid route (duplicate "${HELD}"): ${input}`);
127
+ held = sigil === "+" ? "required" : "optional";
128
+ rest = rest.slice(nameMatch[0].length).trimStart();
129
+ continue;
130
+ }
123
131
  if (!MOD_NAME_SET.has(name)) {
124
132
  throw new Error(`invalid route (unknown modifier "${name}"): ${input}`);
125
133
  }
@@ -129,8 +137,9 @@ function parseRoute(input) {
129
137
  modifiers[name] = sigil === "+" ? "required" : "optional";
130
138
  rest = rest.slice(nameMatch[0].length).trimStart();
131
139
  }
132
- return { phases, gesture: gestureName, arg, target, modifiers };
140
+ return { phases, gesture: gestureName, arg, target, modifiers, ...held ? { held } : {} };
133
141
  }
142
+ var HELD = "held";
134
143
  function parsePhaseList(raw, input) {
135
144
  if (raw === "") throw new Error(`invalid route (empty phase list): ${input}`);
136
145
  const tokens = raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
@@ -204,6 +213,7 @@ function collapseShiftPairs(routes) {
204
213
  }
205
214
  function isShiftTwin(a, b) {
206
215
  if (a.gesture !== b.gesture) return false;
216
+ if (a.held !== b.held) return false;
207
217
  if (a.arg !== b.arg) return false;
208
218
  if (a.target !== b.target) return false;
209
219
  if (!samePhases(a.phases, b.phases)) return false;
@@ -246,6 +256,8 @@ function formatRoute(r) {
246
256
  if (req === "required") out += ` +${name}`;
247
257
  else if (req === "optional") out += ` ?${name}`;
248
258
  }
259
+ if (r.held === "required") out += ` +${HELD}`;
260
+ else if (r.held === "optional") out += ` ?${HELD}`;
249
261
  return out;
250
262
  }
251
263
 
@@ -474,6 +486,7 @@ function matchSpec(e, spec, isMac, phaseCtx) {
474
486
  case "hover": {
475
487
  if (e.kind !== "hover") return false;
476
488
  if (!matchModifiers(e, spec.mods, isMac)) return false;
489
+ if (!e.off && (e.held === true ? spec.held === void 0 : spec.held === true)) return false;
477
490
  const on = !e.off && matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
478
491
  if (spec.motion !== "leave") return on;
479
492
  return !on && e.from !== void 0 && matchTarget(e.from.affordance, spec.target, e.from.bodyTarget, e.from.bodyKind);
@@ -503,8 +516,10 @@ function matchSpec(e, spec, isMac, phaseCtx) {
503
516
  function matchArrival(e, spec, isMac) {
504
517
  if (e.kind !== spec.kind) return false;
505
518
  if (e.kind === "carry") {
506
- if (e.stage !== "enter" && e.stage !== "drop") return false;
519
+ const target = "target" in spec ? spec.target : void 0;
520
+ if (e.stage === "leave" || e.stage === "move" && target === void 0) return false;
507
521
  if (!matchIngestTypes([{ mime: e.type }], spec.types)) return false;
522
+ if (!matchTarget(e.affordance, target, e.bodyTarget, e.bodyKind)) return false;
508
523
  } else if (e.kind === "drop" || e.kind === "paste") {
509
524
  if (!matchIngestTypes(e.items, spec.types)) return false;
510
525
  }
@@ -574,7 +589,12 @@ function routeToSpec(parsed) {
574
589
  case void 0:
575
590
  return fail(`${parsed.gesture} has no GestureSpec kind`);
576
591
  case "hover":
577
- return withMods({ kind, ...arg === "leave" ? { motion: "leave" } : {}, ...t });
592
+ return withMods({
593
+ kind,
594
+ ...arg === "leave" ? { motion: "leave" } : {},
595
+ ...parsed.held ? { held: parsed.held === "required" ? true : "optional" } : {},
596
+ ...t
597
+ });
578
598
  case "click":
579
599
  case "pointerDown":
580
600
  case "doubleClick":
@@ -598,8 +618,9 @@ function routeToSpec(parsed) {
598
618
  case "multiTouchTap":
599
619
  if (arg === void 0) return fail(`a ${kind} spec needs concrete fingers`);
600
620
  return withMods({ kind, fingers: Number(arg) });
601
- case "drop":
602
621
  case "carry":
622
+ return withMods({ kind, ...arg ? { types: arg.split("|") } : {}, ...t });
623
+ case "drop":
603
624
  case "paste":
604
625
  return withMods({ kind, ...arg ? { types: arg.split("|") } : {} });
605
626
  }
@@ -669,6 +690,11 @@ function targetClause(target, hasTarget) {
669
690
  if (target === "empty") return " on empty canvas";
670
691
  return ` on a ${target}`;
671
692
  }
693
+ function carryOnto(target) {
694
+ if (target === void 0 || target === "*") return "onto the canvas";
695
+ if (target === "empty") return "onto empty canvas";
696
+ return `onto a ${target}`;
697
+ }
672
698
  function concreteArg(parsed) {
673
699
  return parsed.arg === "*" ? void 0 : parsed.arg;
674
700
  }
@@ -709,8 +735,11 @@ function actionClause(parsed, required) {
709
735
  return userDoes(required, `clicks${target}`);
710
736
  case "pointerDown":
711
737
  return userDoes(required, `presses${target}`);
712
- case "hover":
713
- return userDoes(required, arg === "leave" ? `moves the pointer ${leftClause(parsed.target)}` : `moves the pointer${target}`);
738
+ case "hover": {
739
+ const held = parsed.held === "required" ? " with a button held" : parsed.held === "optional" ? " whether or not a button is held" : "";
740
+ const where = arg === "leave" ? ` ${leftClause(parsed.target)}` : target;
741
+ return userDoes(required, `moves the pointer${where}${held}`);
742
+ }
714
743
  case "dblTap":
715
744
  return userDoes(required, `double-taps${target}`);
716
745
  case "drag":
@@ -718,7 +747,7 @@ function actionClause(parsed, required) {
718
747
  case "drop":
719
748
  return userDoes(required, `drops ${arg ?? "any"} content onto the canvas`);
720
749
  case "carry":
721
- return userDoes(required, `carries ${arg ?? "anything"} onto the canvas`);
750
+ return userDoes(required, `carries ${arg ?? "anything"} ${carryOnto(parsed.target)}`);
722
751
  case "paste":
723
752
  return userDoes(required, `pastes ${arg ?? "any"} content`);
724
753
  }
@@ -892,6 +921,45 @@ function keySpecsFromShortcut(shortcut, opts = {}) {
892
921
  return key === void 0 ? keys : [...keys, keySpecFromKey(key, opts)];
893
922
  }
894
923
 
924
+ // src/ui/keySpecFromCode.ts
925
+ var MOD_CODES = {
926
+ ShiftLeft: "shift",
927
+ ShiftRight: "shift",
928
+ ControlLeft: "ctrl",
929
+ ControlRight: "ctrl",
930
+ AltLeft: "alt",
931
+ AltRight: "alt",
932
+ MetaLeft: "meta",
933
+ MetaRight: "meta"
934
+ };
935
+ var PRINTED = {
936
+ Backquote: "`",
937
+ Minus: "-",
938
+ Equal: "=",
939
+ BracketLeft: "[",
940
+ BracketRight: "]",
941
+ Backslash: "\\",
942
+ Semicolon: ";",
943
+ Quote: "'",
944
+ Comma: ",",
945
+ Period: ".",
946
+ Slash: "/",
947
+ IntlBackslash: "\xA7",
948
+ CapsLock: "Caps",
949
+ Fn: "fn"
950
+ };
951
+ function keySpecFromCode(code, opts = {}) {
952
+ const own = opts.labels?.[code];
953
+ if (own !== void 0) return { label: own };
954
+ const mod = MOD_CODES[code];
955
+ if (mod) return keySpecsFromMods([{ name: mod }], opts)[0];
956
+ const char = /^Key([A-Z])$/.exec(code) ?? /^Digit(\d)$/.exec(code);
957
+ if (char) return keySpecFromKey(char[1], opts);
958
+ if (code === "NumpadEnter") return keySpecFromKey("Enter", opts);
959
+ const printed = PRINTED[code];
960
+ return printed !== void 0 ? { label: printed } : keySpecFromKey(code, opts);
961
+ }
962
+
895
963
  // src/ui/formatShortcut.ts
896
964
  var labels = (b, opts) => b ? keySpecsFromShortcut({ ...b, shift: b.shift === true }, opts)?.map((k) => k.label) : void 0;
897
965
  function formatShortcutParts(b, opts = {}) {
@@ -1191,6 +1259,13 @@ function thresholdsOf(opts) {
1191
1259
  })
1192
1260
  };
1193
1261
  }
1262
+ function sampleTime(clock) {
1263
+ return (s) => {
1264
+ const now = clock.now();
1265
+ const t = s.timeStamp;
1266
+ return t !== void 0 && t > 0 && t <= now ? t : now;
1267
+ };
1268
+ }
1194
1269
  function createPointerRecognizer(opts) {
1195
1270
  const { sink, clock, observer = {}, toSurface = (p) => p } = opts;
1196
1271
  const world = (s) => opts.toWorld?.(s.clientX, s.clientY) ?? { x: s.clientX, y: s.clientY };
@@ -1202,12 +1277,13 @@ function createPointerRecognizer(opts) {
1202
1277
  observer.holdChanged?.(pending);
1203
1278
  });
1204
1279
  const geometry = (extra = []) => heldGeometry(held, toSurface, extra);
1205
- const claimForMultiTouch = (mods2) => {
1280
+ const happenedAt = sampleTime(clock);
1281
+ const claimForMultiTouch = (s) => {
1206
1282
  hold.cancel();
1207
1283
  for (const p of held.values()) p.buffered = null;
1208
1284
  const g = geometry();
1209
- touches.join(positionsOf(held), g.centroid, clock.now());
1210
- sink.dispatch(multiTouchEvent(held.size, mods2, g));
1285
+ touches.join(positionsOf(held), g.centroid, happenedAt(s));
1286
+ sink.dispatch(multiTouchEvent(held.size, s, g));
1211
1287
  };
1212
1288
  return {
1213
1289
  press(s) {
@@ -1251,7 +1327,7 @@ function createPointerRecognizer(opts) {
1251
1327
  if (before >= 2 && held.size < 2) {
1252
1328
  const fingers = touches.finish(
1253
1329
  geometry([{ x: s.clientX, y: s.clientY }]).centroid,
1254
- clock.now(),
1330
+ happenedAt(s),
1255
1331
  longPressMs()
1256
1332
  );
1257
1333
  if (fingers !== null) sink.dispatch({ kind: "multitouchtap", fingers, ...modifiersOf(s) });
@@ -1267,7 +1343,7 @@ function createPointerRecognizer(opts) {
1267
1343
  worldY: at2.y,
1268
1344
  ...modifiersOf(s)
1269
1345
  };
1270
- return composeClick(rec.down, up, clicks, () => clock.now(), (ev) => sink.dispatch(ev), observer, doubleClick());
1346
+ return composeClick(rec.down, up, clicks, () => happenedAt(s), (ev) => sink.dispatch(ev), observer, doubleClick());
1271
1347
  },
1272
1348
  cancel(pointerId) {
1273
1349
  const rec = held.get(pointerId);
@@ -1421,6 +1497,6 @@ function focusedControlOwnsKey(e) {
1421
1497
  return activatesFocusedControl(e) || selectOwnsKey(e);
1422
1498
  }
1423
1499
 
1424
- export { ACTIVE_SIGILS, CONTROL_SELECTOR, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, GESTURE_DESCRIPTORS, LONG_PRESS_MS, MODIFIER_LABELS, MOD_NAME_SET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, TAP_THRESHOLD_PX, UNKNOWN_MIME, VALID_MOD_NAMES, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, detectPlatform, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, formatShortcut, formatShortcutParts, getGestureDescriptor, heldPointers, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, keySpecFromKey, keySpecsFromMods, keySpecsFromShortcut, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
1500
+ export { ACTIVE_SIGILS, CONTROL_SELECTOR, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, GESTURE_DESCRIPTORS, LONG_PRESS_MS, MODIFIER_LABELS, MOD_NAME_SET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, TAP_THRESHOLD_PX, UNKNOWN_MIME, VALID_MOD_NAMES, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, detectPlatform, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, formatShortcut, formatShortcutParts, getGestureDescriptor, heldPointers, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, keySpecFromCode, keySpecFromKey, keySpecsFromMods, keySpecsFromShortcut, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
1425
1501
  //# sourceMappingURL=index.js.map
1426
1502
  //# sourceMappingURL=index.js.map