@sigx/lynx-gestures 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Andreas Ekdahl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,358 @@
1
+ # @sigx/lynx-gestures
2
+
3
+ Declarative, **frame-locked** gesture and animation primitives for [SignalX](https://github.com/signalxjs) on Lynx. Touch handlers, drag/swipe components, and animation linkage all run on the platform's main UI thread — your gestures track the finger at the display refresh rate even when the JS thread is busy fetching, parsing, or re-rendering.
4
+
5
+ ## Features
6
+
7
+ - **Built-in gesture components** — `<Pressable>`, `<Draggable>`, `<Swipeable>` — drop in for instant 60/120 fps interactions, no worklet plumbing in user code.
8
+ - **Main-Thread Scripting under the hood** — touch handlers, transform updates, and visual feedback run on Lynx's main thread (Lepus) so gestures don't block on your background JS.
9
+ - **Background-thread composables** — `useTap`, `useLongPress`, `usePan`, `usePinch`, `useSwipe`, `useRotation`, `useFling`, `usePanResponder`, and a `useGesture` composer with simultaneous / exclusive / sequential relations.
10
+ - **Composition utilities** — `mergeHandlers`, gesture composers, render-prop slots for swipe-to-reveal actions.
11
+
12
+ > **Cross-thread primitive moved + renamed.** `useSharedValue`, `SharedValue` (formerly `useAnimatedValue` / `AnimatedValue`), and `useAnimatedStyle` were promoted to [`@sigx/lynx`](../lynx) in 0.3.0 — they have no gesture coupling and now live next to `MainThreadRef` (their base class). The primitive was renamed in Phase 2.8 to reflect that it's a general MT/BG bridge — animation is one customer; gestures and scroll are equally first-class. The old `useAnimatedValue` / `AnimatedValue` import paths still work via deprecated re-exports for one minor cycle; please import `useSharedValue` / `SharedValue` from `@sigx/lynx` directly in new code.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @sigx/lynx-gestures
18
+ ```
19
+
20
+ > Requires `@sigx/lynx` as a peer dependency. The build pipeline (`@sigx/lynx-plugin`) handles the `'main thread'` worklet transform automatically.
21
+
22
+ ## Quick start
23
+
24
+ ```tsx
25
+ import { signal, component, useSharedValue } from '@sigx/lynx';
26
+ import { Pressable, Draggable, Swipeable } from '@sigx/lynx-gestures';
27
+
28
+ const App = component(() => {
29
+ const taps = signal(0);
30
+ const dragX = useSharedValue(0);
31
+
32
+ return () => (
33
+ <view>
34
+ {/* Tap with instant visual feedback */}
35
+ <Pressable
36
+ pressedOpacity={0.5}
37
+ pressedScale={0.95}
38
+ onPress={() => { taps.value++; }}
39
+ style={{ width: '100px', height: '100px', backgroundColor: '#3b82f6' }}
40
+ />
41
+
42
+ {/* Drag at native frame rate; observe position on BG */}
43
+ <Draggable
44
+ translateX={dragX}
45
+ snapBack
46
+ onDragEnd={(e) => console.log('released at', e.x, e.y)}
47
+ style={{ width: '90px', height: '90px', backgroundColor: '#a855f7' }}
48
+ />
49
+ <text>BG sees x = {dragX.value}</text>
50
+
51
+ {/* Swipe-to-reveal */}
52
+ <Swipeable
53
+ rightActions={() => <view><text>Delete</text></view>}
54
+ onSwipeOpen={(e) => console.log('opened', e.side)}
55
+ >
56
+ <view><text>Swipe me</text></view>
57
+ </Swipeable>
58
+ </view>
59
+ );
60
+ });
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Why this exists — the architecture
66
+
67
+ ### The two-thread model
68
+
69
+ Lynx runs your app on two JS contexts:
70
+
71
+ - **Background (BG) thread** — your component code, signals, effects, fetch/parse, JSX renders.
72
+ - **Main (MT) thread** — the renderer's commit thread, where native draw calls happen.
73
+
74
+ A naive touch handler runs on BG, mutates a signal, triggers a re-render, the renderer diffs styles, queues an op, the op crosses to MT, MT commits. **Two thread crossings per touchmove**, plus a JSX render and a JSON marshal. At 120 Hz touch input, that pipeline can't keep up — the cursor visibly lags the finger and chatters under GC pressure.
75
+
76
+ ### How `@sigx/lynx-gestures` solves it
77
+
78
+ Gesture components mark their touch handlers as `'main thread'` worklets. The build pipeline extracts those handlers, ships them to MT once at startup, and Lynx native dispatches touch events directly to them. The handler then mutates a `SharedValue` (a thread-aware ref) and calls `setStyleProperties` on the bound element — all on the MT thread, **zero crossings**, no JSX render, no JSON.
79
+
80
+ ### Cross-thread observability
81
+
82
+ When you pass a `SharedValue` to a gesture component, the MT thread continuously writes to it. A bridge publishes those writes to the BG thread once per native flush (typically per frame), where they land in a `signal`-style mirror. Your `effect(() => sv.value)` re-runs reactively without injecting BG into the gesture hot path.
83
+
84
+ ```
85
+ MT thread: tx.current.value = 50 ─┐
86
+ MT thread: setStyleProperties(...) │ one event per flush
87
+ ↓ __FlushElementTree │ with [wvid, value] tuples
88
+ ↓ flushAvBridgePublishes ────────┘
89
+ BG thread: signal value = 50 → effect re-runs, debounce, fetch, etc.
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Built-in components
95
+
96
+ ### `<Pressable>`
97
+
98
+ Tap and long-press with optional visual feedback. The opacity and scale flash apply on MT inside the touchstart worklet, so feedback is visually instantaneous.
99
+
100
+ ```tsx
101
+ <Pressable
102
+ pressedOpacity={0.5}
103
+ pressedScale={0.95}
104
+ longPressDuration={500}
105
+ onPress={() => doThing()}
106
+ onLongPress={() => doOtherThing()}
107
+ style={{ ... }}
108
+ >
109
+ <text>Press me</text>
110
+ </Pressable>
111
+ ```
112
+
113
+ | Prop | Type | Default | Description |
114
+ | -------------------- | --------- | ------- | -------------------------------------------------------- |
115
+ | `pressedOpacity` | `number` | — | Opacity to apply on press, restored on release. |
116
+ | `pressedScale` | `number` | — | `scale()` factor on press, restored on release. |
117
+ | `longPressDuration` | `number` | `500` | ms to hold before `onLongPress` fires. |
118
+ | `maxDistance` | `number` | `10` | Move threshold (px) above which press is cancelled. |
119
+ | `disabled` | `boolean` | `false` | Suppresses both events and visual feedback. |
120
+ | `onPress` | event | — | Fires on tap (touchend within `maxDistance`). |
121
+ | `onLongPress` | event | — | Fires after `longPressDuration` if still pressed. |
122
+
123
+ ### `<Draggable>`
124
+
125
+ Pan-to-translate on the MT thread, with optional axis lock, bounds clamping, snap-back, and `SharedValue` exposure of the position.
126
+
127
+ ```tsx
128
+ const tx = useSharedValue(0);
129
+ const ty = useSharedValue(0);
130
+
131
+ <Draggable
132
+ axis="both"
133
+ threshold={4}
134
+ snapBack
135
+ minX={-100} maxX={100}
136
+ translateX={tx} translateY={ty}
137
+ onDragStart={(e) => console.log('start', e.x, e.y)}
138
+ onDragEnd={(e) => console.log('end', e.x, e.y, 'velocity', e.vx, e.vy)}
139
+ >
140
+ <view style={{ width: '90px', height: '90px', backgroundColor: '#a855f7' }} />
141
+ </Draggable>
142
+ ```
143
+
144
+ | Prop | Type | Default | Description |
145
+ | ------------------- | ------------------------------- | ------- | -------------------------------------------------------- |
146
+ | `axis` | `'x' \| 'y' \| 'both'` | `'both'`| Restrict motion to one axis. |
147
+ | `threshold` | `number` | `0` | Min distance (px) before recognition fires. |
148
+ | `snapBack` | `boolean` | `false` | Animate back to origin on release. |
149
+ | `minX`/`maxX`/`minY`/`maxY` | `number` | — | Clamp the translation range. |
150
+ | `translateX` | `SharedValue<number>` | — | External SharedValue the worklet writes on every touchmove. |
151
+ | `translateY` | `SharedValue<number>` | — | Same, for the Y axis. |
152
+ | `onDragStart` | event `{ x, y }` | — | Fires once per gesture after threshold is met. |
153
+ | `onDragEnd` | event `{ x, y, vx, vy }` | — | Fires on release; includes terminal velocity. |
154
+
155
+ ### `<Swipeable>`
156
+
157
+ Horizontal swipe-to-reveal with up to two action panels. Uses `MTElementWrapper.animate()` for the snap, so the easing curve runs on the native compositor.
158
+
159
+ ```tsx
160
+ <Swipeable
161
+ leftActions={() => <view style={{ backgroundColor: '#22c55e' }}><text>Archive</text></view>}
162
+ rightActions={() => <view style={{ backgroundColor: '#ef4444' }}><text>Delete</text></view>}
163
+ onSwipeOpen={(e) => console.log('opened', e.side)}
164
+ onSwipeClose={() => console.log('closed')}
165
+ >
166
+ <view><text>Row content</text></view>
167
+ </Swipeable>
168
+ ```
169
+
170
+ | Prop | Type | Default | Description |
171
+ | --------------------- | -------------------------------- | ------- | -------------------------------------------------------- |
172
+ | `leftActionsWidth` | `number` | `100` | Width (px) of the left reveal panel. |
173
+ | `rightActionsWidth` | `number` | `100` | Width (px) of the right reveal panel. |
174
+ | `snapThreshold` | `number` | `40` | Min translation before snapping to the open position. |
175
+ | `snapDuration` | `number` | `200` | Snap animation duration (ms). |
176
+ | `leftActions` | `() => JSX` | — | Render-prop for the left panel. |
177
+ | `rightActions` | `() => JSX` | — | Render-prop for the right panel. |
178
+ | `onSwipeOpen` | event `{ side: 'left' \| 'right' }` | — | Fires when the row snaps open. |
179
+ | `onSwipeClose` | event | — | Fires when the row snaps closed from an open position. |
180
+
181
+ ### `<ScrollView>`
182
+
183
+ MT-thread `<scroll-view>` wrapper that mirrors scroll position into a `SharedValue`. Pair with `useAnimatedStyle` for parallax / fade / scale effects driven by scroll — all running on MT with zero per-frame thread crossings.
184
+
185
+ ```tsx
186
+ import { useSharedValue, useAnimatedStyle, useMainThreadRef } from '@sigx/lynx';
187
+ import { ScrollView } from '@sigx/lynx-gestures';
188
+
189
+ const scrollY = useSharedValue(0);
190
+ const headerRef = useMainThreadRef<MainThread.Element | null>(null);
191
+
192
+ useAnimatedStyle(headerRef, scrollY, 'translateY', {
193
+ inputRange: [0, 300], outputRange: [0, -150], extrapolate: 'clamp',
194
+ });
195
+
196
+ <ScrollView offsetY={scrollY}>
197
+ <view main-thread:ref={headerRef}><image src={hero} /></view>
198
+ <text>Body…</text>
199
+ <text>Scroll: {scrollY.value.toFixed(0)}px</text>
200
+ </ScrollView>
201
+ ```
202
+
203
+ | Prop | Type | Default | Description |
204
+ | --------------------- | -------------------------------- | ------------ | -------------------------------------------------------- |
205
+ | `offsetY` | `SharedValue<number>` | — | External SharedValue the worklet writes on every scroll. |
206
+ | `offsetX` | `SharedValue<number>` | — | Same, for the horizontal axis. |
207
+ | `scroll-orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Pass-through to `<scroll-view>`. |
208
+ | `class` / `style` | string / object | — | Pass-through styling. |
209
+
210
+ The component handles the inline `'main thread'` worklet, the SharedValue writes, and the `__FlushElementTree()` trigger internally. Users only see `SharedValue`s.
211
+
212
+ ---
213
+
214
+ ## Animation primitives
215
+
216
+ > The cross-thread primitive — `useSharedValue`, `SharedValue`, `useAnimatedStyle` — lives in [`@sigx/lynx`](../lynx#sharedvalue--the-cross-thread-primitive) since 0.3.0. Import from `@sigx/lynx` directly:
217
+
218
+ ### `useSharedValue<T>(initial)` *(from `@sigx/lynx`)*
219
+
220
+ Allocates a thread-aware value: writeable on MT, reactively observable on BG.
221
+
222
+ ```tsx
223
+ import { useSharedValue } from '@sigx/lynx';
224
+
225
+ const tx = useSharedValue(0);
226
+
227
+ // MT (inside a 'main thread' worklet)
228
+ tx.current.value = 50;
229
+
230
+ // BG (in component body, effect, computed, JSX)
231
+ console.log(tx.value);
232
+ effect(() => console.log('tx is now', tx.value));
233
+ ```
234
+
235
+ `sv.current.value` is the MT-side read/write path (the underlying `MainThreadRef` envelope). `sv.value` is the BG-side reactive read. Writes on BG are read-only (a dev warning fires); the canonical mutation path is the MT worklet.
236
+
237
+ The bridge coalesces writes per native flush — N MT mutations within one frame land as one BG event with N tuples.
238
+
239
+ ### `useAnimatedStyle(elRef, sv, mapperName, params?)` *(from `@sigx/lynx`)*
240
+
241
+ Bind an element's style to a `SharedValue` via a named mapper. The mapper runs on MT every flush where the SharedValue's value changed.
242
+
243
+ ```tsx
244
+ import { useMainThreadRef, useSharedValue, useAnimatedStyle } from '@sigx/lynx';
245
+
246
+ const tx = useSharedValue(0);
247
+ const ghostRef = useMainThreadRef<MainThread.Element | null>(null);
248
+
249
+ useAnimatedStyle(ghostRef, tx, 'translateX', { factor: 0.5 });
250
+ useAnimatedStyle(ghostRef, tx, 'opacity', { factor: -0.01, offset: 1 });
251
+
252
+ <Draggable translateX={tx} />
253
+ <view main-thread:ref={ghostRef} style={{ ... }} />
254
+ ```
255
+
256
+ The ghost view tracks the draggable at half speed and fades as it moves — without a single thread crossing per frame.
257
+
258
+ ### Built-in mappers
259
+
260
+ `translateX`, `translateY`, `scale`, and `opacity` accept **either** a linear `{ factor, offset }` shape **or** a range-mapping `{ inputRange, outputRange, extrapolate? }` shape (see "Range mapping" below).
261
+
262
+ | Name | Linear param shape | Output |
263
+ | ------------ | ------------------------------------------ | --------------------------------------------------- |
264
+ | `translateX` | `{ factor?: number }` | `transform: translateX(value * factor)px` |
265
+ | `translateY` | `{ factor?: number }` | `transform: translateY(value * factor)px` |
266
+ | `translate` | `{ factorX?: number; factorY?: number }` | `transform: translate(v.x*fx, v.y*fy)px` (2D SharedValue) |
267
+ | `scale` | `{ offset?: number }` | `transform: scale(value + offset)` |
268
+ | `opacity` | `{ factor?: number; offset?: number }` | `opacity` clamped to `[0, 1]` of `value*f + o` |
269
+ | `rotate` | (none) | `transform: rotate(value)deg` |
270
+
271
+ When multiple bindings on the same element produce a `transform`, the parts concatenate in registration order. Other style keys merge; later registrations win on duplicate keys. Whenever **any** binding on an element ticks, **all** of its bindings re-run so partial outputs don't drop the unchanged-axis contribution.
272
+
273
+ ### Range mapping
274
+
275
+ `translateX` / `translateY` / `scale` / `opacity` also accept `{ inputRange, outputRange, extrapolate? }` — handy for scroll-driven UIs:
276
+
277
+ ```tsx
278
+ const { y, onScroll } = useScrollViewOffset();
279
+ const headerRef = useMainThreadRef<MainThread.Element | null>(null);
280
+
281
+ // Parallax: scroll 0..300 → translateY 0..-150, clamped beyond.
282
+ useAnimatedStyle(headerRef, y, 'translateY', {
283
+ inputRange: [0, 300], outputRange: [0, -150], extrapolate: 'clamp',
284
+ });
285
+ ```
286
+
287
+ Multi-stop ranges (length ≥ 2) work — each segment is interpolated independently. `extrapolate: 'clamp'` (default) caps at the endpoints; `'identity'` extends linearly using the slope of the nearest segment.
288
+
289
+ ### Custom mappers
290
+
291
+ You can register additional mappers from MT-side code:
292
+
293
+ ```tsx
294
+ // in a 'main thread'-marked module
295
+ import { registerMapper } from '@sigx/lynx-runtime-main';
296
+
297
+ registerMapper('skewX', (v) => ({ transform: `skewX(${v}deg)` }));
298
+ ```
299
+
300
+ Then use the name from BG: `useAnimatedStyle(elRef, sv, 'skewX')`. The string name is what crosses the build pipeline; the function lives on MT.
301
+
302
+ ---
303
+
304
+ ## Background-thread composables
305
+
306
+ For cases where you don't need MT-thread tracking (state machines that drive non-visual logic, gestures over scroll lists, or coordinating multiple recognizers at once), the package also ships background-thread recognizers exposing `signal`-based state.
307
+
308
+ | Composable | Returns |
309
+ | ----------------- | ------------------------------------------------ |
310
+ | `useTap` | tap state + handlers; `onTap`, `onDoubleTap` |
311
+ | `useLongPress` | long-press detection |
312
+ | `usePan` | drag distance / velocity |
313
+ | `usePinch` | scale, focal point |
314
+ | `useSwipe` | direction + distance |
315
+ | `useRotation` | two-finger rotation in radians |
316
+ | `useFling` | velocity-gated flick |
317
+ | `usePanResponder` | RN-shape `onStartShouldSet` / `onMove` / etc. |
318
+ | `useGesture` | composer (simultaneous / exclusive / sequential) |
319
+
320
+ ```tsx
321
+ const pan = usePan({
322
+ onMove: (state) => console.log(state.dx, state.dy),
323
+ });
324
+
325
+ <view {...pan.handlers} />
326
+ ```
327
+
328
+ These are simpler to compose and fully introspectable on BG, at the cost of a thread crossing per gesture event. For visual feedback (translate, scale, opacity), prefer the MT components above.
329
+
330
+ ---
331
+
332
+ ## Performance notes
333
+
334
+ - **Avoid changing the gesture component's `style` prop on every render.** A BG-side `SET_STYLE` op for the same element being dragged can clobber MT-side `setStyleProperties` writes. The framework guards this with shallow-equal in the style patcher, so structurally-stable inline styles (`style={{ width: '90px', ... }}`) are fine. Computed-per-render styles touching the dragged element are the case to watch.
335
+ - **Pass MT-locals through `runOnBackground` arguments**, not through closure capture. The BG-bound function only sees what crossed the bridge — its parameter list. Capturing a `let side = …` declared inside the worklet body will fail at runtime with `ReferenceError` because BG never had `side`.
336
+ - **Per-SharedValue `===` diff coalescing** means object-typed SharedValues (`useSharedValue<{x,y}>`) only publish on identity change, not on property mutation. Use scalar SharedValues and compose them, or use the `translate` mapper which takes a 2D value.
337
+
338
+ ---
339
+
340
+ ## Testing
341
+
342
+ The package ships with two test layers:
343
+
344
+ - **Source-shape regex tests** verify that `'main thread'` directives, handler attribute spellings, and worklet captures are all in place. Fast; run as part of `pnpm test`.
345
+ - **MT end-to-end tests** (`pnpm --filter @sigx/lynx-gestures test:mt`) actually run the SWC LEPUS transform on the live source, eval the resulting `registerWorkletInternal` calls into the upstream worklet runtime under vitest, and drive synthetic touches through the registered worklets — catching the class of bug where a refactor breaks the worklet pipeline silently.
346
+
347
+ ---
348
+
349
+ ## Related
350
+
351
+ - [`@sigx/lynx`](../lynx) — the framework barrel; import everything from here.
352
+ - [`@sigx/runtime-lynx`](../runtime-lynx) — background-thread renderer and signal/effect wiring.
353
+ - [`@sigx/lynx-runtime-main`](../runtime-lynx-main) — main-thread runtime and PAPI integration.
354
+ - [`@sigx/lynx-plugin`](../lynx-plugin) — the rspack/rspeedy plugin that runs the worklet transform at build time.
355
+
356
+ ## License
357
+
358
+ MIT
@@ -0,0 +1,66 @@
1
+ import { type SharedValue, type Define } from '@sigx/lynx';
2
+ export interface DragEndDetail {
3
+ x: number;
4
+ y: number;
5
+ vx: number;
6
+ vy: number;
7
+ }
8
+ /**
9
+ * Edge-scroll configuration for `<Draggable edgeScroll>`. Either `true` for
10
+ * default tuning, or an object overriding the defaults.
11
+ */
12
+ export type EdgeScrollConfig = boolean | {
13
+ /** Distance from viewport edge in pt where auto-scroll engages. Default 50. */
14
+ threshold?: number;
15
+ /** Maximum scroll velocity in pt/sec at the edge. Default 800. */
16
+ maxSpeed?: number;
17
+ };
18
+ export type DraggableProps = Define.Prop<'axis', 'x' | 'y' | 'both', false> & Define.Prop<'threshold', number, false> & Define.Prop<'snapBack', boolean, false> & Define.Prop<'minX', number, false> & Define.Prop<'maxX', number, false> & Define.Prop<'minY', number, false> & Define.Prop<'maxY', number, false> & Define.Prop<'translateX', SharedValue<number>, false> & Define.Prop<'translateY', SharedValue<number>, false> & Define.Prop<'edgeScroll', EdgeScrollConfig, false> & Define.Prop<'class', string, false> & Define.Prop<'style', Record<string, string | number>, false> & Define.Slot<'default'> & Define.Event<'dragStart', {
19
+ x: number;
20
+ y: number;
21
+ }> & Define.Event<'dragEnd', DragEndDetail>;
22
+ /**
23
+ * MT-thread draggable container, built on the native gesture arena via
24
+ * `Gesture.Pan()`. The bound element's transform is driven by two
25
+ * `useAnimatedStyle` bindings (one per axis) — the same primitive any user
26
+ * could compose. The Pan onUpdate worklet writes to the SharedValues; the
27
+ * bridge applies the transform on the next flush boundary, composing the
28
+ * two bindings into a single `setStyleProperties({ transform })` call.
29
+ *
30
+ * Because the visible position is bridge-driven rather than written directly
31
+ * by the worklet, external animation of `translateX`/`translateY` (e.g.
32
+ * `withSpring(tx, 0)` to spring back to origin after release) moves the
33
+ * element visually for free — the binding picks up whichever SV write
34
+ * happened most recently, regardless of who wrote it.
35
+ *
36
+ * `dragStart` and `dragEnd` are dispatched to BG via `runOnBackground` (low
37
+ * frequency, cross-thread is fine).
38
+ *
39
+ * Unlike the prior `bindtouch*`-based implementation, the native pan gesture
40
+ * arena handles multi-touch correctly (secondary fingers don't cancel the
41
+ * primary drag).
42
+ *
43
+ * **Scroll composition** (Phase 2.12.3): Lynx's `<scroll-view>` doesn't
44
+ * participate in the new gesture arena, so without coordination both pan
45
+ * and scroll would fire concurrently. `<Draggable>` reads `useScrollContext`
46
+ * at setup; if a parent `<ScrollView>` is in scope, the BG-side dragStart/
47
+ * dragEnd flips `scrollCtx.dragging` automatically — the parent's
48
+ * `enable-scroll` is gated on that signal, so the UIKit pan recognizer
49
+ * yields for the duration of the drag. No consumer wiring required.
50
+ *
51
+ * **Edge-scroll** (Phase 2.13): pass `edgeScroll` to auto-scroll the parent
52
+ * `<ScrollView>` when the finger nears its viewport edge during a drag —
53
+ * the standard drag-to-reorder pattern (Apple Mail, iOS Reminders). The
54
+ * scroll axis follows `scrollOrientation` as published through the context.
55
+ * Inside the threshold zone the scroll velocity ramps from 0 at the
56
+ * threshold boundary to `maxSpeed` at the edge. Quietly no-ops if
57
+ * `edgeScroll` is unset OR the Draggable isn't nested in a ScrollView.
58
+ *
59
+ * Note on the native event payload: Lynx's pan handler emits `pageX`/`pageY`
60
+ * but no `translationX`/`velocityX` — we compute deltas and velocity from
61
+ * pageX/pageY ourselves (same as the prior touch-based implementation).
62
+ */
63
+ export declare const Draggable: import("@sigx/runtime-core").ComponentFactory<DraggableProps, void, {
64
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
65
+ }>;
66
+ //# sourceMappingURL=Draggable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Draggable.d.ts","sourceRoot":"","sources":["../../src/components/Draggable.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,WAAW,EAChB,KAAK,MAAM,EAEZ,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,aAAa;IAC5B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG;IACvC,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GACtB,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,MAAM,EAAE,KAAK,CAAC,GAC9C,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,GACvC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,GACvC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,GAClC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,GAClC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,GAClC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,GAClC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,GACrD,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,GACrD,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,gBAAgB,EAAE,KAAK,CAAC,GAClD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,GACnC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,GAC5D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,GACtB,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACnD,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;AAkC3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,SAAS;;EAuUpB,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { type Define } from '@sigx/lynx';
2
+ export type PressableProps = Define.Prop<'pressedOpacity', number, false> & Define.Prop<'pressedScale', number, false> & Define.Prop<'longPressDuration', number, false> & Define.Prop<'maxDistance', number, false> & Define.Prop<'disabled', boolean, false> & Define.Prop<'class', string, false> & Define.Prop<'style', Record<string, string | number>, false> & Define.Slot<'default'> & Define.Event<'press', void> & Define.Event<'longPress', void>;
3
+ /**
4
+ * MT-thread tap + long-press recognizer with built-in pressed-state visual
5
+ * feedback (opacity + scale). Press and long-press callbacks are dispatched
6
+ * to BG via `runOnBackground` (low-frequency cross-thread is fine).
7
+ *
8
+ * Cross-platform gesture-arena quirks (Phase 2.12.1, observed on iOS Lynx
9
+ * 3.5 sim and Android Lynx 3.6 / Pixel 9 Pro XL) make this component a
10
+ * hybrid: it composes `Gesture.Tap()` + `Gesture.LongPress()` via
11
+ * `Simultaneous` AND adds an onEnd-fallback path inside LongPress, so press
12
+ * emission works on both platforms via different routes:
13
+ *
14
+ * - **Android**: `Tap.onStart` fires on touch-up (as documented). Press
15
+ * emits there; the LongPress fallback sees `pressEmitted=true` and
16
+ * skips. `Tap.onEnd` fires on the same touch-up — but iOS's premature
17
+ * onEnd (next bullet) means we can't safely reset styles here, so style
18
+ * reset lives in LongPress.onEnd.
19
+ * - **iOS**: `Tap.onEnd` fires ~6ms after touchstart (an arena
20
+ * fail/reset path that doesn't trigger on Android). `Tap.onStart`
21
+ * never fires for our composition. We rely on `LongPress.onEnd` to
22
+ * detect "lift before duration with no movement" and emit press from
23
+ * the fallback. `Gesture.Race` would be simpler in theory, but its
24
+ * `waitFor` deadlocks Tap on iOS — the arena dispatches Tap before
25
+ * LongPress reaches Fail state.
26
+ *
27
+ * State tracks `longPressFired` and `pressEmitted` so neither event
28
+ * double-fires regardless of which platform path resolves first.
29
+ * Movement past `maxDistance` is tracked from `e.params.pageX/pageY`;
30
+ * `LongPress.onEnd` skips press emission when the touch drifted past
31
+ * the threshold (matching Tap's success criteria).
32
+ *
33
+ * Disabled is captured at setup; runtime toggling won't update an active
34
+ * gesture's behavior. Wrap the parent in conditional rendering for now if
35
+ * dynamic disable is needed.
36
+ */
37
+ export declare const Pressable: import("@sigx/runtime-core").ComponentFactory<PressableProps, void, {
38
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
39
+ }>;
40
+ //# sourceMappingURL=Pressable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Pressable.d.ts","sourceRoot":"","sources":["../../src/components/Pressable.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,MAAM,EAEZ,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GACtB,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,EAAE,KAAK,CAAC,GAC5C,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,CAAC,GAC1C,MAAM,CAAC,IAAI,CAAC,mBAAmB,EAAE,MAAM,EAAE,KAAK,CAAC,GAC/C,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,CAAC,GACzC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,GACvC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,GACnC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,GAC5D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,GACtB,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,GAC3B,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;AASpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,SAAS;;EA+GpB,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { type SharedValue, type Define } from '@sigx/lynx';
2
+ export type ScrollViewProps = Define.Prop<'offsetX', SharedValue<number>, false> & Define.Prop<'offsetY', SharedValue<number>, false> & Define.Prop<'scroll-orientation', 'vertical' | 'horizontal', false>
3
+ /**
4
+ * Toggle native scroll responsiveness at runtime — set false to lock the
5
+ * scroll-view (e.g. while a child `<Draggable>` is mid-drag, so Lynx's
6
+ * native pan gesture doesn't steal the touch). Maps to Lynx's
7
+ * `enable-scroll` attribute.
8
+ */
9
+ & Define.Prop<'enable-scroll', boolean, false> & Define.Prop<'class', string, false> & Define.Prop<'style', Record<string, string | number>, false> & Define.Slot<'default'>;
10
+ /**
11
+ * MT-thread `<scroll-view>` wrapper that mirrors scroll position into a
12
+ * `SharedValue`. Pair with `useAnimatedStyle` for parallax / fade / scale
13
+ * effects driven by scroll, all running on MT with zero per-frame thread
14
+ * crossings.
15
+ *
16
+ * The component is the API; the inline `'main thread'` worklet, the
17
+ * `__FlushElementTree()` trigger, and the runtime registration are all
18
+ * internal. Users just pass a `SharedValue<number>` for the axis they care
19
+ * about — same shape as `<Draggable translateX={tx}>`.
20
+ *
21
+ * @example Parallax header
22
+ * ```tsx
23
+ * const scrollY = useSharedValue(0);
24
+ * const headerRef = useMainThreadRef<MainThread.Element | null>(null);
25
+ *
26
+ * useAnimatedStyle(headerRef, scrollY, 'translateY', {
27
+ * inputRange: [0, 300], outputRange: [0, -150], extrapolate: 'clamp',
28
+ * });
29
+ *
30
+ * <ScrollView offsetY={scrollY}>
31
+ * <view main-thread:ref={headerRef}><image src={hero} /></view>
32
+ * <text>Body…</text>
33
+ * </ScrollView>
34
+ * ```
35
+ *
36
+ * @example BG-reactive scroll readout
37
+ * ```tsx
38
+ * const scrollY = useSharedValue(0);
39
+ * <ScrollView offsetY={scrollY}>...</ScrollView>
40
+ * <text>Scrolled: {scrollY.value.toFixed(0)}px</text>
41
+ * ```
42
+ */
43
+ export declare const ScrollView: import("@sigx/runtime-core").ComponentFactory<ScrollViewProps, void, {
44
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
45
+ }>;
46
+ //# sourceMappingURL=ScrollView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollView.d.ts","sourceRoot":"","sources":["../../src/components/ScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,MAAM,EAEZ,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,eAAe,GACvB,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,GAClD,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,GAClD,MAAM,CAAC,IAAI,CAAC,oBAAoB,EAAE,UAAU,GAAG,YAAY,EAAE,KAAK,CAAC;AACrE;;;;;GAKG;GACD,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,OAAO,EAAE,KAAK,CAAC,GAC5C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,GACnC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,GAC5D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,UAAU;;EA8DrB,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { type Define } from '@sigx/lynx';
2
+ export type SwipeSide = 'left' | 'right';
3
+ export type SwipeableProps = Define.Prop<'leftActionsWidth', number, false> & Define.Prop<'rightActionsWidth', number, false> & Define.Prop<'snapThreshold', number, false> & Define.Prop<'snapDuration', number, false> & Define.Prop<'leftActions', () => unknown, false> & Define.Prop<'rightActions', () => unknown, false> & Define.Prop<'class', string, false> & Define.Prop<'style', Record<string, string | number>, false> & Define.Prop<'foregroundStyle', Record<string, string | number>, false> & Define.Slot<'default'> & Define.Event<'swipeOpen', {
4
+ side: SwipeSide;
5
+ }> & Define.Event<'swipeClose', void>;
6
+ /**
7
+ * Horizontal swipe-to-reveal container, built on the native gesture arena
8
+ * via `Gesture.Pan().axis('x')`. The foreground is dragged horizontally on
9
+ * the MT thread; on release it snaps to one of three resting positions
10
+ * (closed / open-left / open-right) using `MTElementWrapper.animate()`.
11
+ * Open and close events are dispatched to BG via `runOnBackground`.
12
+ *
13
+ * Migrated from a 4-`bindtouch*`-worklet implementation to a single
14
+ * `Gesture.Pan()` (Phase 2.12). Carries the same Phase 2.11 quirks:
15
+ * - `.onBegin(() => {})` no-op is load-bearing on iOS Pan to gate
16
+ * `_isInvokedBegin` open so onStart/onEnd fire.
17
+ * - `e.params.pageX` (not `e.pageX`) — Lynx pan event nests the touch
18
+ * payload under `params`.
19
+ *
20
+ * Supply `leftActions` and/or `rightActions` as render-prop functions:
21
+ *
22
+ * ```tsx
23
+ * <Swipeable
24
+ * rightActions={() => <view><text>Delete</text></view>}
25
+ * onSwipeOpen={(e) => console.log('opened', e.side)}
26
+ * >
27
+ * <view><text>Row content</text></view>
28
+ * </Swipeable>
29
+ * ```
30
+ *
31
+ * **Scroll composition** (Phase 2.12.3): nesting `<Swipeable>` inside
32
+ * `<ScrollView>` is automatic — `useScrollContext` is read at setup and
33
+ * the BG-side onStart/onEnd handlers flip `scrollCtx.dragging` so the
34
+ * parent yields its UIKit pan for the duration of the swipe. No consumer
35
+ * wiring required.
36
+ */
37
+ export declare const Swipeable: import("@sigx/runtime-core").ComponentFactory<SwipeableProps, void, {
38
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
39
+ }>;
40
+ //# sourceMappingURL=Swipeable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Swipeable.d.ts","sourceRoot":"","sources":["../../src/components/Swipeable.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,MAAM,EAEZ,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AAEzC,MAAM,MAAM,cAAc,GACtB,MAAM,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,EAAE,KAAK,CAAC,GAC9C,MAAM,CAAC,IAAI,CAAC,mBAAmB,EAAE,MAAM,EAAE,KAAK,CAAC,GAC/C,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,KAAK,CAAC,GAC3C,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,CAAC,GAC1C,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,OAAO,EAAE,KAAK,CAAC,GAChD,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,OAAO,EAAE,KAAK,CAAC,GACjD,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,GACnC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,GAC5D,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,GACtE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,GACtB,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC9C,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;AASrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,SAAS;;EAwJpB,CAAC"}
@@ -0,0 +1,16 @@
1
+ export { usePinch } from './use-pinch.js';
2
+ export { useRotation } from './use-rotation.js';
3
+ export { useSharedValue, SharedValue, useAnimatedValue, AnimatedValue, useAnimatedStyle, resetAnimatedStyleBindingIds, } from '@sigx/lynx';
4
+ export type { SharedValueState, AnimatedValueState, BuiltinMapperName, MapperParams, } from '@sigx/lynx';
5
+ export { Pressable } from './components/Pressable.js';
6
+ export type { PressableProps } from './components/Pressable.js';
7
+ export { Draggable } from './components/Draggable.js';
8
+ export type { DraggableProps, DragEndDetail } from './components/Draggable.js';
9
+ export { Swipeable } from './components/Swipeable.js';
10
+ export type { SwipeableProps, SwipeSide } from './components/Swipeable.js';
11
+ export { ScrollView } from './components/ScrollView.js';
12
+ export type { ScrollViewProps } from './components/ScrollView.js';
13
+ export { useScrollContext } from './scroll-context.js';
14
+ export type { ScrollContext } from './scroll-context.js';
15
+ export type { TouchPoint, TouchEvent, GesturePhase, GestureHandlers, PinchState, UsePinchOptions, UsePinchReturn, RotationState, UseRotationOptions, UseRotationReturn, } from './types.js';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAOhD,OAAO,EACL,cAAc,EACd,WAAW,EAEX,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,gBAAgB,EAEhB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,GACb,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAKlE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,eAAe,EACf,UAAU,EACV,eAAe,EACf,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}