@mindees/core 0.4.0 → 0.6.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,133 @@
1
+ //#region src/gesture/recognizers.d.ts
2
+ /**
3
+ * Gesture recognizers — RN Gesture Handler / Flutter GestureDetector parity, built on the reactive
4
+ * core. Each factory returns a {@link Recognizer}: a bag of pointer-event handlers to spread onto a
5
+ * host element, plus REACTIVE state (signals) you read in a `style` accessor or feed to the
6
+ * animation engine. The only platform-aware code is {@link normalizePointer}; everything else is
7
+ * pure payload → signal, so it runs on web (Pointer Events), native (the command-backend payload),
8
+ * and tests (synthetic events), and SSRs safely (no `document` access).
9
+ *
10
+ * Attach two recognizers to ONE element via {@link composeGestures} — the renderer binds a single
11
+ * listener per event name, so spreading two `onPointerMove`s would drop one; compose merges them.
12
+ *
13
+ * @module
14
+ */
15
+ /** A normalized pointer sample (platform differences absorbed by {@link normalizePointer}). */
16
+ interface PointerSample {
17
+ readonly pointerId: number;
18
+ readonly x: number;
19
+ readonly y: number;
20
+ /** Timestamp in ms. */
21
+ readonly t: number;
22
+ readonly pointerType?: string;
23
+ }
24
+ /** @internal Test-only: inject a deterministic clock + timer. */
25
+ declare function _setGestureClock(opts: {
26
+ now?: () => number;
27
+ schedule?: (fn: () => void, ms: number) => () => void;
28
+ }): void;
29
+ /** Normalize a host pointer event to a {@link PointerSample}. Web `PointerEvent` or a native payload. */
30
+ declare function normalizePointer(e: unknown): PointerSample;
31
+ /** Handlers to spread onto a host element. */
32
+ interface GestureHandlers {
33
+ onPointerDown(e: unknown): void;
34
+ onPointerMove(e: unknown): void;
35
+ onPointerUp(e: unknown): void;
36
+ onPointerCancel(e: unknown): void;
37
+ }
38
+ /** A recognizer: handlers to attach, reactive `state`, and a `reset()` for cleanup. */
39
+ interface Recognizer<S = Record<string, () => number | boolean>> {
40
+ readonly handlers: GestureHandlers;
41
+ readonly state: S;
42
+ /** Clear pointers, timers, and reset state to rest (call from `onCleanup`). */
43
+ reset(): void;
44
+ }
45
+ /** A continuous pan/drag update (translations relative to gesture start; velocity in px/ms). */
46
+ interface PanEvent {
47
+ readonly translationX: number;
48
+ readonly translationY: number;
49
+ readonly velocityX: number;
50
+ readonly velocityY: number;
51
+ readonly x: number;
52
+ readonly y: number;
53
+ }
54
+ interface PanState {
55
+ readonly active: () => boolean;
56
+ readonly translationX: () => number;
57
+ readonly translationY: () => number;
58
+ readonly velocityX: () => number;
59
+ readonly velocityY: () => number;
60
+ readonly x: () => number;
61
+ readonly y: () => number;
62
+ }
63
+ /** Recognize a drag. Becomes active once the pointer moves past `minDistance` (slop). */
64
+ declare function pan(config: {
65
+ onBegin?: (e: PanEvent) => void;
66
+ onUpdate?: (e: PanEvent) => void;
67
+ onEnd?: (e: PanEvent & {
68
+ completed: boolean;
69
+ }) => void;
70
+ minDistance?: number;
71
+ axis?: 'both' | 'x' | 'y';
72
+ }): Recognizer<PanState>;
73
+ interface TapState {
74
+ readonly active: () => boolean;
75
+ }
76
+ /** Recognize a tap: down + up within `maxDistance` and `maxDurationMs`, no extra pointer. */
77
+ declare function tap(config: {
78
+ onTap?: () => void;
79
+ maxDistance?: number;
80
+ maxDurationMs?: number;
81
+ }): Recognizer<TapState>;
82
+ /** Recognize a long press: pointer held past `minDurationMs` without moving past `maxDistance`. */
83
+ declare function longPress(config: {
84
+ onBegin?: () => void;
85
+ onLongPress?: () => void;
86
+ onEnd?: () => void;
87
+ minDurationMs?: number;
88
+ maxDistance?: number;
89
+ }): Recognizer<TapState>;
90
+ interface PinchEvent {
91
+ readonly scale: number;
92
+ readonly velocity: number;
93
+ readonly focalX: number;
94
+ readonly focalY: number;
95
+ }
96
+ interface PinchState {
97
+ readonly active: () => boolean;
98
+ readonly scale: () => number;
99
+ readonly focalX: () => number;
100
+ readonly focalY: () => number;
101
+ }
102
+ /** Recognize a two-finger pinch. `scale` is current distance / start distance between the pinned pair. */
103
+ declare function pinch(config: {
104
+ onBegin?: (e: PinchEvent) => void;
105
+ onUpdate?: (e: PinchEvent) => void;
106
+ onEnd?: (e: PinchEvent) => void;
107
+ }): Recognizer<PinchState>;
108
+ type SwipeDirection = 'left' | 'right' | 'up' | 'down';
109
+ interface SwipeEvent {
110
+ readonly direction: SwipeDirection;
111
+ readonly velocityX: number;
112
+ readonly velocityY: number;
113
+ readonly x: number;
114
+ readonly y: number;
115
+ }
116
+ /** Recognize a fast flick on pointer-up (velocity ≥ `minVelocity` px/ms over `minDistance`). */
117
+ declare function swipe(config: {
118
+ onSwipe?: (e: SwipeEvent) => void;
119
+ direction?: 'any' | SwipeDirection;
120
+ minVelocity?: number;
121
+ minDistance?: number;
122
+ }): Recognizer<TapState>;
123
+ /**
124
+ * Merge several recognizers into ONE so they can attach to a single element — required because the
125
+ * renderer binds a single listener per event name (spreading two `onPointerMove`s would drop one).
126
+ * `simultaneous` (the default): every recognizer sees every event independently (e.g. pan + pinch
127
+ * together). Per-recognizer slop already disambiguates tap-vs-pan; an explicit exclusive arena is a
128
+ * follow-up.
129
+ */
130
+ declare function composeGestures(recognizers: readonly Recognizer<never>[]): Recognizer<never>;
131
+ //#endregion
132
+ export { GestureHandlers, PanEvent, PanState, PinchEvent, PinchState, PointerSample, Recognizer, SwipeDirection, SwipeEvent, TapState, _setGestureClock, composeGestures, longPress, normalizePointer, pan, pinch, swipe, tap };
133
+ //# sourceMappingURL=recognizers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recognizers.d.ts","names":[],"sources":["../../src/gesture/recognizers.ts"],"mappings":";;AAiBA;;;;;;;;;;AAMsB;AAWtB;;UAjBiB,aAAA;EAAA,SACN,SAAA;EAAA,SACA,CAAA;EAAA,SACA,CAAA;EAgBG;EAAA,SAdH,CAAA;EAAA,SACA,WAAA;AAAA;AAcV;AAAA,iBAHe,gBAAA,CAAiB,IAAA;EAC/B,GAAA;EACA,QAAA,IAAY,EAAA,cAAgB,EAAA;AAAA;AAO6B;AAAA,iBAA3C,gBAAA,CAAiB,CAAA,YAAa,aAAa;;UAgB1C,eAAA;EACf,aAAA,CAAc,CAAA;EACd,aAAA,CAAc,CAAA;EACd,WAAA,CAAY,CAAA;EACZ,eAAA,CAAgB,CAAA;AAAA;;UAID,UAAA,KAAe,MAAA;EAAA,SACrB,QAAA,EAAU,eAAA;EAAA,SACV,KAAA,EAAO,CAAA;EANU;EAQ1B,KAAA;AAAA;;UAoDe,QAAA;EAAA,SACN,YAAA;EAAA,SACA,YAAA;EAAA,SACA,SAAA;EAAA,SACA,SAAA;EAAA,SACA,CAAA;EAAA,SACA,CAAA;AAAA;AAAA,UAGM,QAAA;EAAA,SACN,MAAA;EAAA,SACA,YAAA;EAAA,SACA,YAAA;EAAA,SACA,SAAA;EAAA,SACA,SAAA;EAAA,SACA,CAAA;EAAA,SACA,CAAA;AAAA;;iBAIK,GAAA,CAAI,MAAA;EAClB,OAAA,IAAW,CAAA,EAAG,QAAA;EACd,QAAA,IAAY,CAAA,EAAG,QAAA;EACf,KAAA,IAAS,CAAA,EAAG,QAAA;IAAa,SAAA;EAAA;EACzB,WAAA;EACA,IAAA;AAAA,IACE,UAAA,CAAW,QAAA;AAAA,UAqHE,QAAA;EAAA,SACN,MAAM;AAAA;;iBAID,GAAA,CAAI,MAAA;EAClB,KAAA;EACA,WAAA;EACA,aAAA;AAAA,IACE,UAAU,CAAC,QAAA;;iBAoDC,SAAA,CAAU,MAAA;EACxB,OAAA;EACA,WAAA;EACA,KAAA;EACA,aAAA;EACA,WAAA;AAAA,IACE,UAAU,CAAC,QAAA;AAAA,UA0DE,UAAA;EAAA,SACN,KAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;EAAA,SACA,MAAA;AAAA;AAAA,UAEM,UAAA;EAAA,SACN,MAAA;EAAA,SACA,KAAA;EAAA,SACA,MAAA;EAAA,SACA,MAAA;AAAA;;iBAIK,KAAA,CAAM,MAAA;EACpB,OAAA,IAAW,CAAA,EAAG,UAAA;EACd,QAAA,IAAY,CAAA,EAAG,UAAA;EACf,KAAA,IAAS,CAAA,EAAG,UAAA;AAAA,IACV,UAAA,CAAW,UAAA;AAAA,KAuHH,cAAA;AAAA,UACK,UAAA;EAAA,SACN,SAAA,EAAW,cAAc;EAAA,SACzB,SAAA;EAAA,SACA,SAAA;EAAA,SACA,CAAA;EAAA,SACA,CAAA;AAAA;;iBAIK,KAAA,CAAM,MAAA;EACpB,OAAA,IAAW,CAAA,EAAG,UAAA;EACd,SAAA,WAAoB,cAAA;EACpB,WAAA;EACA,WAAA;AAAA,IACE,UAAA,CAAW,QAAA;;;;;;;AA5QQ;iBA2UP,eAAA,CAAgB,WAAA,WAAsB,UAAA,YAAsB,UAAU"}
@@ -0,0 +1,507 @@
1
+ import { batch, signal } from "../reactive/reactive.js";
2
+ //#region src/gesture/recognizers.ts
3
+ /**
4
+ * Gesture recognizers — RN Gesture Handler / Flutter GestureDetector parity, built on the reactive
5
+ * core. Each factory returns a {@link Recognizer}: a bag of pointer-event handlers to spread onto a
6
+ * host element, plus REACTIVE state (signals) you read in a `style` accessor or feed to the
7
+ * animation engine. The only platform-aware code is {@link normalizePointer}; everything else is
8
+ * pure payload → signal, so it runs on web (Pointer Events), native (the command-backend payload),
9
+ * and tests (synthetic events), and SSRs safely (no `document` access).
10
+ *
11
+ * Attach two recognizers to ONE element via {@link composeGestures} — the renderer binds a single
12
+ * listener per event name, so spreading two `onPointerMove`s would drop one; compose merges them.
13
+ *
14
+ * @module
15
+ */
16
+ let nowMs = () => Date.now();
17
+ let scheduleTimer = (fn, ms) => {
18
+ const id = setTimeout(fn, ms);
19
+ return () => clearTimeout(id);
20
+ };
21
+ /** @internal Test-only: inject a deterministic clock + timer. */
22
+ function _setGestureClock(opts) {
23
+ if (opts.now) nowMs = opts.now;
24
+ if (opts.schedule) scheduleTimer = opts.schedule;
25
+ }
26
+ /** Normalize a host pointer event to a {@link PointerSample}. Web `PointerEvent` or a native payload. */
27
+ function normalizePointer(e) {
28
+ const ev = e ?? {};
29
+ const web = "clientX" in ev;
30
+ const num = (v, d = 0) => typeof v === "number" && Number.isFinite(v) ? v : d;
31
+ return {
32
+ pointerId: num(ev.pointerId, 1),
33
+ x: web ? num(ev.clientX) : num(ev.x),
34
+ y: web ? num(ev.clientY) : num(ev.y),
35
+ t: num(web ? ev.timeStamp : ev.timestamp, nowMs()),
36
+ ...typeof (web ? ev.pointerType : ev.type) === "string" ? { pointerType: web ? ev.pointerType : ev.type } : {}
37
+ };
38
+ }
39
+ const VEL_ALPHA = .6;
40
+ const dist = (ax, ay, bx, by) => Math.hypot(ax - bx, ay - by);
41
+ /** Update a tracked pointer + its EWMA velocity from a new sample. */
42
+ function track(p, s) {
43
+ const dtMs = s.t - p.lastT;
44
+ if (dtMs > 0) {
45
+ p.vx = VEL_ALPHA * ((s.x - p.lastX) / dtMs) + (1 - VEL_ALPHA) * p.vx;
46
+ p.vy = VEL_ALPHA * ((s.y - p.lastY) / dtMs) + (1 - VEL_ALPHA) * p.vy;
47
+ }
48
+ p.lastX = s.x;
49
+ p.lastY = s.y;
50
+ p.lastT = s.t;
51
+ p.x = s.x;
52
+ p.y = s.y;
53
+ }
54
+ function newTracked(s) {
55
+ return {
56
+ startX: s.x,
57
+ startY: s.y,
58
+ startT: s.t,
59
+ x: s.x,
60
+ y: s.y,
61
+ lastX: s.x,
62
+ lastY: s.y,
63
+ lastT: s.t,
64
+ vx: 0,
65
+ vy: 0
66
+ };
67
+ }
68
+ /** Recognize a drag. Becomes active once the pointer moves past `minDistance` (slop). */
69
+ function pan(config) {
70
+ const minDistance = config.minDistance ?? 10;
71
+ const axis = config.axis ?? "both";
72
+ const pointers = /* @__PURE__ */ new Map();
73
+ let id = null;
74
+ let active = false;
75
+ const active$ = signal(false);
76
+ const tx$ = signal(0);
77
+ const ty$ = signal(0);
78
+ const vx$ = signal(0);
79
+ const vy$ = signal(0);
80
+ const x$ = signal(0);
81
+ const y$ = signal(0);
82
+ const filt = (dx, dy) => axis === "x" ? [dx, 0] : axis === "y" ? [0, dy] : [dx, dy];
83
+ const eventFor = (p) => {
84
+ const [tx, ty] = filt(p.x - p.startX, p.y - p.startY);
85
+ const [vx, vy] = filt(p.vx, p.vy);
86
+ return {
87
+ translationX: tx,
88
+ translationY: ty,
89
+ velocityX: vx,
90
+ velocityY: vy,
91
+ x: p.x,
92
+ y: p.y
93
+ };
94
+ };
95
+ const writeFrom = (p) => {
96
+ const e = eventFor(p);
97
+ tx$.set(e.translationX);
98
+ ty$.set(e.translationY);
99
+ vx$.set(e.velocityX);
100
+ vy$.set(e.velocityY);
101
+ x$.set(e.x);
102
+ y$.set(e.y);
103
+ };
104
+ const reset = () => {
105
+ pointers.clear();
106
+ id = null;
107
+ active = false;
108
+ batch(() => {
109
+ active$.set(false);
110
+ tx$.set(0);
111
+ ty$.set(0);
112
+ vx$.set(0);
113
+ vy$.set(0);
114
+ });
115
+ };
116
+ return {
117
+ state: {
118
+ active: () => active$(),
119
+ translationX: () => tx$(),
120
+ translationY: () => ty$(),
121
+ velocityX: () => vx$(),
122
+ velocityY: () => vy$(),
123
+ x: () => x$(),
124
+ y: () => y$()
125
+ },
126
+ reset,
127
+ handlers: {
128
+ onPointerDown(e) {
129
+ const s = normalizePointer(e);
130
+ pointers.set(s.pointerId, newTracked(s));
131
+ if (id === null) id = s.pointerId;
132
+ },
133
+ onPointerMove(e) {
134
+ const s = normalizePointer(e);
135
+ const p = pointers.get(s.pointerId);
136
+ if (!p) return;
137
+ track(p, s);
138
+ if (s.pointerId !== id) return;
139
+ const [sx, sy] = filt(p.x - p.startX, p.y - p.startY);
140
+ const slop = Math.hypot(sx, sy);
141
+ batch(() => {
142
+ if (!active && slop >= minDistance) {
143
+ active = true;
144
+ active$.set(true);
145
+ writeFrom(p);
146
+ config.onBegin?.(eventFor(p));
147
+ } else if (active) {
148
+ writeFrom(p);
149
+ config.onUpdate?.(eventFor(p));
150
+ }
151
+ });
152
+ },
153
+ onPointerUp(e) {
154
+ const s = normalizePointer(e);
155
+ const p = pointers.get(s.pointerId);
156
+ if (p) track(p, s);
157
+ if (s.pointerId === id && active && p) config.onEnd?.({
158
+ ...eventFor(p),
159
+ completed: true
160
+ });
161
+ pointers.delete(s.pointerId);
162
+ if (s.pointerId === id) {
163
+ id = pointers.keys().next().value ?? null;
164
+ active = false;
165
+ active$.set(false);
166
+ }
167
+ },
168
+ onPointerCancel(e) {
169
+ const s = normalizePointer(e);
170
+ const p = pointers.get(s.pointerId);
171
+ if (s.pointerId === id && active && p) config.onEnd?.({
172
+ ...eventFor(p),
173
+ completed: false
174
+ });
175
+ pointers.delete(s.pointerId);
176
+ if (s.pointerId === id) {
177
+ id = pointers.keys().next().value ?? null;
178
+ active = false;
179
+ active$.set(false);
180
+ }
181
+ }
182
+ }
183
+ };
184
+ }
185
+ /** Recognize a tap: down + up within `maxDistance` and `maxDurationMs`, no extra pointer. */
186
+ function tap(config) {
187
+ const maxDistance = config.maxDistance ?? 10;
188
+ const maxDurationMs = config.maxDurationMs ?? 500;
189
+ let down = null;
190
+ let failed = false;
191
+ const active$ = signal(false);
192
+ const reset = () => {
193
+ down = null;
194
+ failed = false;
195
+ active$.set(false);
196
+ };
197
+ return {
198
+ state: { active: () => active$() },
199
+ reset,
200
+ handlers: {
201
+ onPointerDown(e) {
202
+ if (down !== null) {
203
+ failed = true;
204
+ return;
205
+ }
206
+ down = normalizePointer(e);
207
+ failed = false;
208
+ active$.set(true);
209
+ },
210
+ onPointerMove(e) {
211
+ if (!down || failed) return;
212
+ const s = normalizePointer(e);
213
+ if (s.pointerId === down.pointerId && dist(s.x, s.y, down.x, down.y) > maxDistance) {
214
+ failed = true;
215
+ active$.set(false);
216
+ }
217
+ },
218
+ onPointerUp(e) {
219
+ const s = normalizePointer(e);
220
+ if (down && !failed && s.pointerId === down.pointerId) {
221
+ if (dist(s.x, s.y, down.x, down.y) <= maxDistance && s.t - down.t <= maxDurationMs) config.onTap?.();
222
+ }
223
+ if (!down || s.pointerId === down.pointerId) reset();
224
+ },
225
+ onPointerCancel(e) {
226
+ const s = normalizePointer(e);
227
+ if (!down || s.pointerId === down.pointerId) reset();
228
+ }
229
+ }
230
+ };
231
+ }
232
+ /** Recognize a long press: pointer held past `minDurationMs` without moving past `maxDistance`. */
233
+ function longPress(config) {
234
+ const minDurationMs = config.minDurationMs ?? 500;
235
+ const maxDistance = config.maxDistance ?? 10;
236
+ let down = null;
237
+ let cancelTimer = null;
238
+ let fired = false;
239
+ const active$ = signal(false);
240
+ const clear = () => {
241
+ cancelTimer?.();
242
+ cancelTimer = null;
243
+ };
244
+ const reset = () => {
245
+ clear();
246
+ down = null;
247
+ fired = false;
248
+ active$.set(false);
249
+ };
250
+ /** End the press, firing onEnd iff the long-press had fired, then reset. */
251
+ const finish = () => {
252
+ if (fired) config.onEnd?.();
253
+ reset();
254
+ };
255
+ return {
256
+ state: { active: () => active$() },
257
+ reset,
258
+ handlers: {
259
+ onPointerDown(e) {
260
+ if (down !== null) return;
261
+ down = normalizePointer(e);
262
+ fired = false;
263
+ active$.set(true);
264
+ config.onBegin?.();
265
+ cancelTimer = scheduleTimer(() => {
266
+ fired = true;
267
+ config.onLongPress?.();
268
+ }, minDurationMs);
269
+ },
270
+ onPointerMove(e) {
271
+ if (!down) return;
272
+ const s = normalizePointer(e);
273
+ if (s.pointerId === down.pointerId && dist(s.x, s.y, down.x, down.y) > maxDistance) finish();
274
+ },
275
+ onPointerUp(e) {
276
+ const s = normalizePointer(e);
277
+ if (down && s.pointerId === down.pointerId) finish();
278
+ },
279
+ onPointerCancel(e) {
280
+ const s = normalizePointer(e);
281
+ if (!down || s.pointerId === down.pointerId) finish();
282
+ }
283
+ }
284
+ };
285
+ }
286
+ /** Recognize a two-finger pinch. `scale` is current distance / start distance between the pinned pair. */
287
+ function pinch(config) {
288
+ const pts = /* @__PURE__ */ new Map();
289
+ let pair = null;
290
+ let startDist = 0;
291
+ let lastScale = 1;
292
+ let lastT = 0;
293
+ const active$ = signal(false);
294
+ const scale$ = signal(1);
295
+ const fx$ = signal(0);
296
+ const fy$ = signal(0);
297
+ const pairPts = () => {
298
+ if (!pair) return null;
299
+ const a = pts.get(pair[0]);
300
+ const b = pts.get(pair[1]);
301
+ return a && b ? [a, b] : null;
302
+ };
303
+ const eventNow = (a, b, t) => {
304
+ const d = dist(a.x, a.y, b.x, b.y);
305
+ const scale = startDist > 0 ? d / startDist : 1;
306
+ const dt = t - lastT;
307
+ const velocity = dt > 0 ? (scale - lastScale) / dt : 0;
308
+ lastScale = scale;
309
+ lastT = t;
310
+ return {
311
+ scale,
312
+ velocity,
313
+ focalX: (a.x + b.x) / 2,
314
+ focalY: (a.y + b.y) / 2
315
+ };
316
+ };
317
+ const reset = () => {
318
+ pts.clear();
319
+ pair = null;
320
+ startDist = 0;
321
+ lastScale = 1;
322
+ batch(() => {
323
+ active$.set(false);
324
+ scale$.set(1);
325
+ fx$.set(0);
326
+ fy$.set(0);
327
+ });
328
+ };
329
+ const begin = () => {
330
+ const pp = pairPts();
331
+ if (!pp) return;
332
+ startDist = dist(pp[0].x, pp[0].y, pp[1].x, pp[1].y);
333
+ lastScale = 1;
334
+ lastT = Math.max(pp[0].t, pp[1].t);
335
+ const focalX = (pp[0].x + pp[1].x) / 2;
336
+ const focalY = (pp[0].y + pp[1].y) / 2;
337
+ batch(() => {
338
+ active$.set(true);
339
+ scale$.set(1);
340
+ fx$.set(focalX);
341
+ fy$.set(focalY);
342
+ });
343
+ config.onBegin?.({
344
+ scale: 1,
345
+ velocity: 0,
346
+ focalX,
347
+ focalY
348
+ });
349
+ };
350
+ /** A pointer left: if it was a pinned finger but ≥2 remain, re-pin survivors continuously; else end. */
351
+ const onLift = (s) => {
352
+ pts.delete(s.pointerId);
353
+ if (!active$() || !pair) return;
354
+ if (!(s.pointerId === pair[0] || s.pointerId === pair[1])) return;
355
+ if (pts.size >= 2) {
356
+ const ids = [...pts.keys()];
357
+ pair = [ids[0], ids[1]];
358
+ const pp = pairPts();
359
+ if (pp) {
360
+ startDist = dist(pp[0].x, pp[0].y, pp[1].x, pp[1].y) / Math.max(scale$(), 1e-4);
361
+ lastScale = scale$();
362
+ }
363
+ return;
364
+ }
365
+ config.onEnd?.({
366
+ scale: scale$(),
367
+ velocity: 0,
368
+ focalX: fx$(),
369
+ focalY: fy$()
370
+ });
371
+ reset();
372
+ };
373
+ return {
374
+ state: {
375
+ active: () => active$(),
376
+ scale: () => scale$(),
377
+ focalX: () => fx$(),
378
+ focalY: () => fy$()
379
+ },
380
+ reset,
381
+ handlers: {
382
+ onPointerDown(e) {
383
+ const s = normalizePointer(e);
384
+ pts.set(s.pointerId, s);
385
+ if (!pair && pts.size >= 2) {
386
+ const ids = [...pts.keys()];
387
+ pair = [ids[0], ids[1]];
388
+ begin();
389
+ }
390
+ },
391
+ onPointerMove(e) {
392
+ const s = normalizePointer(e);
393
+ if (!pts.has(s.pointerId)) return;
394
+ pts.set(s.pointerId, s);
395
+ const pp = pairPts();
396
+ if (!active$() || !pp) return;
397
+ const ev = eventNow(pp[0], pp[1], s.t);
398
+ batch(() => {
399
+ scale$.set(ev.scale);
400
+ fx$.set(ev.focalX);
401
+ fy$.set(ev.focalY);
402
+ });
403
+ config.onUpdate?.(ev);
404
+ },
405
+ onPointerUp(e) {
406
+ onLift(normalizePointer(e));
407
+ },
408
+ onPointerCancel(e) {
409
+ onLift(normalizePointer(e));
410
+ }
411
+ }
412
+ };
413
+ }
414
+ /** Recognize a fast flick on pointer-up (velocity ≥ `minVelocity` px/ms over `minDistance`). */
415
+ function swipe(config) {
416
+ const want = config.direction ?? "any";
417
+ const minVelocity = config.minVelocity ?? .3;
418
+ const minDistance = config.minDistance ?? 30;
419
+ const pointers = /* @__PURE__ */ new Map();
420
+ const active$ = signal(false);
421
+ const reset = () => {
422
+ pointers.clear();
423
+ active$.set(false);
424
+ };
425
+ const dominant = (p) => {
426
+ const dx = p.x - p.startX;
427
+ const dy = p.y - p.startY;
428
+ if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
429
+ return dy >= 0 ? "down" : "up";
430
+ };
431
+ return {
432
+ state: { active: () => active$() },
433
+ reset,
434
+ handlers: {
435
+ onPointerDown(e) {
436
+ const s = normalizePointer(e);
437
+ pointers.set(s.pointerId, newTracked(s));
438
+ active$.set(true);
439
+ },
440
+ onPointerMove(e) {
441
+ const s = normalizePointer(e);
442
+ const p = pointers.get(s.pointerId);
443
+ if (p) track(p, s);
444
+ },
445
+ onPointerUp(e) {
446
+ const s = normalizePointer(e);
447
+ const p = pointers.get(s.pointerId);
448
+ if (p) {
449
+ track(p, s);
450
+ const speed = Math.hypot(p.vx, p.vy);
451
+ const moved = dist(p.x, p.y, p.startX, p.startY);
452
+ const dir = dominant(p);
453
+ if (speed >= minVelocity && moved >= minDistance && (want === "any" || want === dir)) config.onSwipe?.({
454
+ direction: dir,
455
+ velocityX: p.vx,
456
+ velocityY: p.vy,
457
+ x: p.x,
458
+ y: p.y
459
+ });
460
+ }
461
+ pointers.delete(s.pointerId);
462
+ if (pointers.size === 0) active$.set(false);
463
+ },
464
+ onPointerCancel(e) {
465
+ const s = normalizePointer(e);
466
+ pointers.delete(s.pointerId);
467
+ if (pointers.size === 0) active$.set(false);
468
+ }
469
+ }
470
+ };
471
+ }
472
+ /**
473
+ * Merge several recognizers into ONE so they can attach to a single element — required because the
474
+ * renderer binds a single listener per event name (spreading two `onPointerMove`s would drop one).
475
+ * `simultaneous` (the default): every recognizer sees every event independently (e.g. pan + pinch
476
+ * together). Per-recognizer slop already disambiguates tap-vs-pan; an explicit exclusive arena is a
477
+ * follow-up.
478
+ */
479
+ function composeGestures(recognizers) {
480
+ const fan = (key) => (e) => {
481
+ let firstError;
482
+ batch(() => {
483
+ for (const r of recognizers) try {
484
+ r.handlers[key](e);
485
+ } catch (err) {
486
+ if (firstError === void 0) firstError = err;
487
+ }
488
+ });
489
+ if (firstError !== void 0) throw firstError;
490
+ };
491
+ return {
492
+ state: {},
493
+ reset: () => {
494
+ for (const r of recognizers) r.reset();
495
+ },
496
+ handlers: {
497
+ onPointerDown: fan("onPointerDown"),
498
+ onPointerMove: fan("onPointerMove"),
499
+ onPointerUp: fan("onPointerUp"),
500
+ onPointerCancel: fan("onPointerCancel")
501
+ }
502
+ };
503
+ }
504
+ //#endregion
505
+ export { _setGestureClock, composeGestures, longPress, normalizePointer, pan, pinch, swipe, tap };
506
+
507
+ //# sourceMappingURL=recognizers.js.map