@rootnative/impulse 0.0.0-alpha.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/dist/chunk-5BMRKYVY.js +39 -0
  5. package/dist/chunk-F4RHM4ZK.js +77 -0
  6. package/dist/chunk-FR242SUF.js +174 -0
  7. package/dist/chunk-IG5RXCYR.js +74 -0
  8. package/dist/chunk-LM645QQT.js +37 -0
  9. package/dist/chunk-PMR25UCT.js +8 -0
  10. package/dist/chunk-ZX7WNICB.js +39 -0
  11. package/dist/compose/index.d.ts +80 -0
  12. package/dist/compose/index.js +2 -0
  13. package/dist/drag/index.d.ts +277 -0
  14. package/dist/drag/index.js +4 -0
  15. package/dist/gesture-handler/index.d.ts +1 -0
  16. package/dist/gesture-handler/index.js +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +8 -0
  19. package/dist/raw/index.d.ts +63 -0
  20. package/dist/raw/index.js +3 -0
  21. package/dist/tap/index.d.ts +150 -0
  22. package/dist/tap/index.js +4 -0
  23. package/dist/types-Ch2HM3aP.d.ts +142 -0
  24. package/dist/useGestureMemo-Ccv8rB0C.d.ts +38 -0
  25. package/jest-preset.cjs +60 -0
  26. package/jest-setup.cjs +56 -0
  27. package/package.json +115 -0
  28. package/src/compose/index.ts +7 -0
  29. package/src/compose/useGestures.ts +136 -0
  30. package/src/gesture-handler/index.ts +18 -0
  31. package/src/index.ts +53 -0
  32. package/src/intents/drag/index.ts +8 -0
  33. package/src/intents/tap/index.ts +2 -0
  34. package/src/intents/useDrag.ts +563 -0
  35. package/src/intents/useTap.ts +285 -0
  36. package/src/internal/useGestureMemo.ts +110 -0
  37. package/src/internal/useLatestCallback.ts +64 -0
  38. package/src/internal/useStableList.ts +62 -0
  39. package/src/internal/useStableRecord.ts +72 -0
  40. package/src/internal/warnOnce.ts +60 -0
  41. package/src/raw/index.ts +2 -0
  42. package/src/raw/useRawGesture.ts +71 -0
  43. package/src/relations/index.ts +120 -0
  44. package/src/types.ts +187 -0
@@ -0,0 +1,142 @@
1
+ import { RefObject, ComponentType } from 'react';
2
+ import { GestureType, ComposedGesture } from 'react-native-gesture-handler';
3
+ import { SharedValue } from 'react-native-reanimated';
4
+
5
+ /**
6
+ * A gesture that an Impulse gesture can be placed in a relation with.
7
+ *
8
+ * The value is either a ref returned by another Impulse hook (`drag.ref`), a
9
+ * ref on a component that owns a gesture — a `ScrollView`, say — or a raw
10
+ * RNGH gesture object.
11
+ *
12
+ * RNGH models this as `GestureRef` and does **not** export it, so the union
13
+ * is written out here. It is RNGH's own minus the numeric form: RNGH accepts
14
+ * a handler tag as a number for its legacy API, and the three relation
15
+ * methods reject it. `AssertRelationArgument` below is what catches an
16
+ * upstream change to the shape.
17
+ */
18
+ type GestureReference = GestureType | RefObject<GestureType | undefined> | RefObject<ComponentType | undefined | null>;
19
+ /**
20
+ * A gesture that can be handed to `<GestureDetector>`: a single recognizer,
21
+ * or a composition of them.
22
+ *
23
+ * RNGH spells this inline in `GestureDetector`'s props and exports no name
24
+ * for it, so Impulse names it — every hook result's `gesture` has this type,
25
+ * and so does every member `useGestures` accepts.
26
+ */
27
+ type AttachableGesture = GestureType | ComposedGesture;
28
+ /**
29
+ * Extra touchable area around a view, in points. A number widens every edge;
30
+ * an object widens the edges it names.
31
+ *
32
+ * Derived from RNGH's own method signature rather than copied, for the same
33
+ * reason `GestureReference` is: RNGH does not export the type from its package
34
+ * entry, and a hand-written copy is one that absorbs an upstream change
35
+ * without a word.
36
+ */
37
+ type HitSlop = Parameters<GestureType['hitSlop']>[0];
38
+ /** One reference, or several. Every coexistence option accepts both. */
39
+ type GestureReferences = GestureReference | GestureReference[];
40
+ /**
41
+ * How an Impulse gesture coexists with a gesture it does not own — most often
42
+ * a scroll view it lives inside.
43
+ *
44
+ * RNGH exposes this as three methods whose names describe the mechanism
45
+ * (`simultaneousWithExternalGesture`, `blocksExternalGesture`,
46
+ * `requireExternalGestureToFail`) and give no hint about which one a given
47
+ * case wants. These three name the outcome instead. Each maps to exactly one
48
+ * RNGH relation, and they are not interchangeable — picking the wrong one is
49
+ * the single thing consumers get wrong most often.
50
+ *
51
+ * All three may be set at once. They are independent relations, not a choice
52
+ * of one.
53
+ */
54
+ interface CoexistenceOptions {
55
+ /**
56
+ * Both gestures recognize at the same time. Neither waits for the other.
57
+ * Maps to `simultaneousWithExternalGesture`.
58
+ *
59
+ * Use it when the two gestures read different things from the same touch —
60
+ * a pinch and a pan on one image, say.
61
+ */
62
+ alongside?: GestureReferences;
63
+ /**
64
+ * This gesture wins. The named gesture cannot activate until this one has
65
+ * failed. Maps to `blocksExternalGesture`.
66
+ *
67
+ * Use it when this gesture is the foreground affordance — a bottom sheet
68
+ * that must take the drag before the list behind it does.
69
+ */
70
+ blocks?: GestureReferences;
71
+ /**
72
+ * The named gesture wins. This one activates only after that one fails.
73
+ * Maps to `requireExternalGestureToFail`.
74
+ *
75
+ * Use it when this gesture is the fallback — a horizontal drag that should
76
+ * start only once the vertical scroll has declined the touch.
77
+ */
78
+ deferTo?: GestureReferences;
79
+ }
80
+ /**
81
+ * How the members of a `useGestures` composition relate to one another.
82
+ *
83
+ * - `race` — the first to activate wins, and the rest are cancelled.
84
+ * - `simultaneous` — every member recognizes independently.
85
+ * - `exclusive` — members are tried in order, and a later one activates only
86
+ * after every earlier one has failed.
87
+ *
88
+ * Composition is a flat call carrying this mode rather than a nested builder
89
+ * chain, so precedence reads left to right instead of inside out.
90
+ */
91
+ type ComposeMode = 'race' | 'simultaneous' | 'exclusive';
92
+ /**
93
+ * A point in a gesture payload, in points.
94
+ *
95
+ * Grouped rather than spelled as two flat fields, because every payload that
96
+ * carries more than one point — a drag's position and its origin, a pinch's
97
+ * focal point — would otherwise need a prefix per pair and the consumer would
98
+ * pick between `absoluteX` and `focalX` from memory. That flat union is
99
+ * exactly what the intent payloads exist to replace.
100
+ */
101
+ interface Point {
102
+ readonly x: number;
103
+ readonly y: number;
104
+ }
105
+ /**
106
+ * What every intent hook returns: the gesture, a handle other hooks can name
107
+ * in their coexistence options, and whether the gesture is being recognized
108
+ * right now.
109
+ *
110
+ * Each hook extends this with the shared values its own intent produces —
111
+ * `drag.x`, `pinch.scale`, `rotate.angle`. The three members here are the
112
+ * part that is the same whatever the intent.
113
+ */
114
+ interface IntentResult<G extends GestureType> {
115
+ /** The configured gesture. Hand it to `<GestureDetector>`. */
116
+ readonly gesture: G;
117
+ /**
118
+ * A handle on this gesture for another hook's `alongside` / `blocks` /
119
+ * `deferTo`.
120
+ *
121
+ * Prefer it over passing `other.gesture`: a gesture object is replaced when
122
+ * its dependencies change, and this ref is created once and read by RNGH at
123
+ * the moment it resolves relations, so a relation written against it keeps
124
+ * pointing at the live gesture.
125
+ *
126
+ * It is populated when `<GestureDetector>` mounts the gesture, not when the
127
+ * hook runs, so reading `.current` during the first render gives
128
+ * `undefined`.
129
+ */
130
+ readonly ref: RefObject<GestureType | undefined>;
131
+ /**
132
+ * `true` while the gesture is active — for a tap, while the finger is down;
133
+ * for a drag, while it is being dragged.
134
+ *
135
+ * A shared value, so a pressed or grabbed state can be driven on the UI
136
+ * thread through `useAnimatedStyle` without a re-render. Reading `.value`
137
+ * during render works but tells you only what was true at the last commit.
138
+ */
139
+ readonly isActive: SharedValue<boolean>;
140
+ }
141
+
142
+ export type { AttachableGesture as A, CoexistenceOptions as C, GestureReference as G, HitSlop as H, IntentResult as I, Point as P, ComposeMode as a, GestureReferences as b };
@@ -0,0 +1,38 @@
1
+ import { RefObject } from 'react';
2
+ import { GestureType } from 'react-native-gesture-handler';
3
+ import { C as CoexistenceOptions } from './types-Ch2HM3aP.js';
4
+
5
+ /** What every hook built on this helper accepts on top of its own options. */
6
+ interface GestureMemoOptions extends CoexistenceOptions {
7
+ /**
8
+ * A test id for RNGH's `getByGestureTestId`, forwarded to `withTestId`.
9
+ *
10
+ * Impulse's own tests mostly inspect the gesture object directly, which is
11
+ * more precise. This is here for consumers driving their gestures through
12
+ * `fireGestureHandler`, which needs a way to find them.
13
+ */
14
+ testId?: string;
15
+ }
16
+ /** The two members every Impulse gesture hook returns, whatever else it adds. */
17
+ interface BuiltGesture<G extends GestureType> {
18
+ /** The configured gesture. Hand it to `<GestureDetector>`. */
19
+ readonly gesture: G;
20
+ /**
21
+ * A handle on this gesture for another hook's `alongside` / `blocks` /
22
+ * `deferTo`.
23
+ *
24
+ * Passing `other.gesture` to a relation also works — RNGH accepts a gesture
25
+ * object — but it captures *that* object, and a gesture is replaced when
26
+ * its own dependencies change. The ref is created once and never replaced,
27
+ * and RNGH reads it when it resolves relations rather than when the
28
+ * relation is declared, so a relation written against `other.ref` keeps
29
+ * pointing at the live gesture. Prefer it.
30
+ *
31
+ * The ref is populated when `<GestureDetector>` mounts the gesture, not
32
+ * when the hook runs. Reading `.current` during render gives `undefined`
33
+ * on the first pass.
34
+ */
35
+ readonly ref: RefObject<GestureType | undefined>;
36
+ }
37
+
38
+ export type { BuiltGesture as B, GestureMemoOptions as G };
@@ -0,0 +1,60 @@
1
+ // Jest preset for projects consuming `@rootnative/impulse`.
2
+ //
3
+ // Layered on top of `@react-native/jest-preset`. It adds:
4
+ // - the RNGH native-module mocks Impulse's own tests run against, through
5
+ // `jest-setup.cjs`
6
+ // - `transformIgnorePatterns` widened so Jest transforms the published
7
+ // bundle of `@rootnative/impulse` (ESM-only, see tsup.config.ts) and
8
+ // `react-native-gesture-handler`, neither of which the default
9
+ // `react-native` pattern lets through
10
+ //
11
+ // Usage:
12
+ //
13
+ // // jest.config.js
14
+ // module.exports = {
15
+ // preset: require.resolve('@rootnative/impulse/jest-preset'),
16
+ // }
17
+ //
18
+ // To allowlist more packages for transformation, extend
19
+ // `transformIgnorePatterns` in your own config — Jest merges over the preset.
20
+
21
+ // `@react-native/jest-preset` is deliberately **not** a peer dependency of
22
+ // `@rootnative/impulse`. React Native pins it to an exact version (RN 0.86.3
23
+ // requires exactly `@react-native/jest-preset@0.86.3`), so any range declared
24
+ // here would advertise versions that cannot install, and an exact pin would
25
+ // break on every RN patch. The version relationship belongs to
26
+ // `react-native`, which already declares it.
27
+ //
28
+ // Resolve the package directly rather than through the
29
+ // `react-native/jest-preset` shim. RN 0.86 moved the preset into its own
30
+ // package and left the old path as a shim that re-exports it — but it
31
+ // declares the new package as an **optional** peer, and no package manager
32
+ // installs an optional peer. On RN 0.86 the shim therefore throws a migration
33
+ // error for any consumer who has not installed it by hand. Requiring it here
34
+ // means the failure names this package's requirement instead.
35
+ let rnPreset
36
+ try {
37
+ rnPreset = require('@react-native/jest-preset')
38
+ } catch (error) {
39
+ if (error.code === 'MODULE_NOT_FOUND') {
40
+ throw new Error(
41
+ '[impulse] `@rootnative/impulse/jest-preset` needs `@react-native/jest-preset`.\n' +
42
+ 'React Native 0.86 moved its Jest preset into that package and declares\n' +
43
+ 'it as an optional peer, so it is not installed for you. Add it as a\n' +
44
+ 'devDependency at the version that matches your react-native:\n\n' +
45
+ ' npm install --save-dev @react-native/jest-preset\n',
46
+ )
47
+ }
48
+ throw error
49
+ }
50
+
51
+ module.exports = {
52
+ ...rnPreset,
53
+ setupFiles: [
54
+ ...(rnPreset.setupFiles ?? []),
55
+ require.resolve('./jest-setup.cjs'),
56
+ ],
57
+ transformIgnorePatterns: [
58
+ 'node_modules/(?!(react-native|@react-native|@react-native-community|@rootnative/impulse|react-native-gesture-handler|react-native-reanimated|react-native-worklets)/)',
59
+ ],
60
+ }
package/jest-setup.cjs ADDED
@@ -0,0 +1,56 @@
1
+ // Impulse's Jest setup. Loaded automatically by
2
+ // `@rootnative/impulse/jest-preset`, or addable to a hand-rolled config
3
+ // through `setupFiles`.
4
+ //
5
+ // The mock surface is deliberately thin. Impulse drives its tests with RNGH's
6
+ // own `fireGestureHandler` / `getByGestureTestId`, which need RNGH's real
7
+ // gesture objects and its own native-module mocks — not a hand-rolled
8
+ // chainable builder. So this file delegates to the setup RNGH ships and adds
9
+ // nothing that RNGH already covers.
10
+ //
11
+ // `react-native-gesture-handler/jestSetup` mocks:
12
+ // - `RNGestureHandlerModule`, the native module every gesture talks to
13
+ // - `GestureButtons` and `Pressable`, the components that need it
14
+ // It leaves `Gesture.*` and `GestureDetector` real, which is what makes
15
+ // `fireGestureHandler` able to drive a gesture through its state machine.
16
+ require('react-native-gesture-handler/jestSetup')
17
+
18
+ // Reanimated mock — the minimum Impulse's own surface touches, and no more.
19
+ //
20
+ // The stock `react-native-reanimated/mock` is not used, for the reason
21
+ // Inertia hand-rolls its own: its `useSharedValue` returns a fresh object per
22
+ // render. Every Impulse hook hands its `isActive` to a worklet captured in a
23
+ // `useMemo`, so a shared value that loses identity means the gesture writes
24
+ // to an object nobody reads, and `isActive` silently never updates. The test
25
+ // in `useTap.test.tsx` pins that identity, so this mock cannot regress into
26
+ // the stock behaviour unnoticed.
27
+ //
28
+ // Deliberately absent: `useAnimatedStyle`, the `Animated.*` components, and
29
+ // the animation functions. Impulse renders none of them and starts no
30
+ // animation — Principle 5, animation is Inertia's job. Add a member here when
31
+ // a module in `src/` actually imports it, not before.
32
+ //
33
+ // What this means for tests:
34
+ // ✅ assert a shared value's `.value` after driving a gesture
35
+ // ✅ assert that a JS-thread callback fired, because `runOnJS` is identity
36
+ // ❌ frame-level timing is not observable — nothing schedules or animates
37
+ jest.mock('react-native-reanimated', () => {
38
+ const React = require('react')
39
+
40
+ return {
41
+ __esModule: true,
42
+ useSharedValue: (initial) => {
43
+ const ref = React.useRef(null)
44
+ if (ref.current === null) {
45
+ ref.current = { value: initial }
46
+ }
47
+ return ref.current
48
+ },
49
+ runOnJS: (fn) => fn,
50
+ runOnUI: (fn) => fn,
51
+ isWorkletFunction: () => false,
52
+ // Needed by RNGH, not by Impulse. See the note above.
53
+ useEvent: (callback) => callback,
54
+ setGestureState: () => {},
55
+ }
56
+ })
package/package.json ADDED
@@ -0,0 +1,115 @@
1
+ {
2
+ "name": "@rootnative/impulse",
3
+ "version": "0.0.0-alpha.0",
4
+ "description": "Declarative gesture primitives for React Native, built on react-native-gesture-handler.",
5
+ "license": "MIT",
6
+ "author": "RootNative",
7
+ "homepage": "https://github.com/rootnative/impulse",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rootnative/impulse.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/rootnative/impulse/issues"
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "gesture-handler",
19
+ "gesture",
20
+ "swipe",
21
+ "drag",
22
+ "pinch",
23
+ "tap",
24
+ "expo",
25
+ "declarative"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "react-native": "./dist/index.js",
33
+ "source": "./src/index.ts",
34
+ "exports": {
35
+ "./package.json": "./package.json",
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "react-native": "./dist/index.js",
39
+ "default": "./dist/index.js"
40
+ },
41
+ "./compose": {
42
+ "types": "./dist/compose/index.d.ts",
43
+ "react-native": "./dist/compose/index.js",
44
+ "default": "./dist/compose/index.js"
45
+ },
46
+ "./raw": {
47
+ "types": "./dist/raw/index.d.ts",
48
+ "react-native": "./dist/raw/index.js",
49
+ "default": "./dist/raw/index.js"
50
+ },
51
+ "./tap": {
52
+ "types": "./dist/tap/index.d.ts",
53
+ "react-native": "./dist/tap/index.js",
54
+ "default": "./dist/tap/index.js"
55
+ },
56
+ "./drag": {
57
+ "types": "./dist/drag/index.d.ts",
58
+ "react-native": "./dist/drag/index.js",
59
+ "default": "./dist/drag/index.js"
60
+ },
61
+ "./gesture-handler": {
62
+ "types": "./dist/gesture-handler/index.d.ts",
63
+ "react-native": "./dist/gesture-handler/index.js",
64
+ "default": "./dist/gesture-handler/index.js"
65
+ },
66
+ "./jest-preset": "./jest-preset.cjs",
67
+ "./jest-setup": "./jest-setup.cjs"
68
+ },
69
+ "files": [
70
+ "dist",
71
+ "src",
72
+ "jest-preset.cjs",
73
+ "jest-setup.cjs",
74
+ "README.md",
75
+ "LICENSE",
76
+ "CHANGELOG.md",
77
+ "!**/__tests__",
78
+ "!**/*.test.*"
79
+ ],
80
+ "scripts": {
81
+ "build": "tsup",
82
+ "prepack": "tsup",
83
+ "dev": "tsup --watch",
84
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
85
+ "test": "jest",
86
+ "lint": "eslint . --max-warnings 0",
87
+ "clean": "rm -rf dist .turbo *.tsbuildinfo"
88
+ },
89
+ "peerDependencies": {
90
+ "react": ">=19.2.3 <20.0.0",
91
+ "react-native": ">=0.83.0 <0.87.0",
92
+ "react-native-gesture-handler": ">=2.28.0 <3.0.0",
93
+ "react-native-reanimated": ">=4.5.0 <4.6.0",
94
+ "react-native-worklets": ">=0.10.0 <0.11.0"
95
+ },
96
+ "devDependencies": {
97
+ "@react-native/babel-preset": "^0.86.3",
98
+ "@react-native/jest-preset": "^0.86.3",
99
+ "@testing-library/react-native": "^13.3.3",
100
+ "@types/jest": "^29.5.14",
101
+ "@types/react": "^19.2.0",
102
+ "jest": "^29.7.0",
103
+ "react": "19.2.3",
104
+ "react-native": "0.86.3",
105
+ "react-native-gesture-handler": "~2.32.0",
106
+ "react-native-reanimated": "4.5.1",
107
+ "react-native-worklets": "0.10.1",
108
+ "react-test-renderer": "19.2.3",
109
+ "tsup": "^8.3.5",
110
+ "typescript": "^5.7.3"
111
+ },
112
+ "publishConfig": {
113
+ "access": "public"
114
+ }
115
+ }
@@ -0,0 +1,7 @@
1
+ export { useGestures } from './useGestures'
2
+ export type {
3
+ GestureCarrier,
4
+ GestureMember,
5
+ GesturesResult,
6
+ UseGesturesOptions,
7
+ } from './useGestures'
@@ -0,0 +1,136 @@
1
+ import { useMemo } from 'react'
2
+ import { Gesture, type ComposedGesture } from 'react-native-gesture-handler'
3
+ import { warnOnce } from '../internal/warnOnce'
4
+ import { useStableList } from '../internal/useStableList'
5
+ import { type AttachableGesture, type ComposeMode } from '../types'
6
+
7
+ /**
8
+ * Anything carrying a gesture — every Impulse hook result, including the one
9
+ * `useGestures` itself returns.
10
+ */
11
+ export interface GestureCarrier {
12
+ readonly gesture: AttachableGesture
13
+ }
14
+
15
+ /**
16
+ * A member of a composition: a hook result, or a bare gesture from
17
+ * `@rootnative/impulse/gesture-handler`.
18
+ */
19
+ export type GestureMember = GestureCarrier | AttachableGesture
20
+
21
+ /** What `useGestures` returns. It is itself a valid composition member. */
22
+ export interface GesturesResult {
23
+ /** The composed gesture. Hand it to `<GestureDetector>`. */
24
+ readonly gesture: ComposedGesture
25
+ }
26
+
27
+ /** Options for {@link useGestures}. */
28
+ export interface UseGesturesOptions {
29
+ /** How the members relate to one another. */
30
+ mode: ComposeMode
31
+ }
32
+
33
+ /**
34
+ * Compose gestures under one relation.
35
+ *
36
+ * ```tsx
37
+ * // tap and double-tap race; the winner runs alongside the drag
38
+ * const { gesture } = useGestures(
39
+ * [useGestures([tap, double], { mode: 'race' }), drag],
40
+ * { mode: 'simultaneous' },
41
+ * )
42
+ *
43
+ * return (
44
+ * <GestureDetector gesture={gesture}>
45
+ * <View />
46
+ * </GestureDetector>
47
+ * )
48
+ * ```
49
+ *
50
+ * Composition is **data, not nesting**. RNGH expresses the same thing as
51
+ * `Gesture.Simultaneous(Gesture.Race(tap, doubleTap), drag)`, which has to be
52
+ * read backwards to learn what wins and gains a level of nesting per
53
+ * relation. Here each call is one flat list plus the relation that holds over
54
+ * it, and the result is itself a member, so precedence reads left to right
55
+ * and depth is a choice rather than a consequence.
56
+ *
57
+ * The modes:
58
+ *
59
+ * - `race` — the first member to activate wins and cancels the rest. This is
60
+ * what you want for mutually exclusive readings of one touch.
61
+ * - `simultaneous` — every member recognizes independently. A pinch and a
62
+ * rotate on one image.
63
+ * - `exclusive` — members are tried in order, and a later one activates only
64
+ * after every earlier one has failed. A tap and a double-tap, where the tap
65
+ * must wait to learn whether a second one is coming.
66
+ *
67
+ * **Coexistence options are not accepted here, and that is a limitation
68
+ * rather than a choice.** RNGH's three external-gesture relations are methods
69
+ * on a single gesture; a composed gesture does not have them. Put
70
+ * `alongside` / `blocks` / `deferTo` on the member hooks instead — Impulse
71
+ * cannot apply them for you without mutating gestures another hook owns and
72
+ * memoised.
73
+ *
74
+ * **Accessibility.** A composition is as reachable as its members, which is
75
+ * to say a screen reader and a keyboard see none of it. Each member hook
76
+ * documents its own fallback; a composition needs the union of them.
77
+ *
78
+ * @param members - The gestures to compose, in precedence order. The order
79
+ * matters for `exclusive` and is ignored by the other two modes.
80
+ * @param options - The relation to compose under.
81
+ * @returns The composed gesture, stable while the members and the mode are.
82
+ */
83
+ export function useGestures(
84
+ members: readonly GestureMember[],
85
+ options: UseGesturesOptions,
86
+ ): GesturesResult {
87
+ const { mode } = options
88
+
89
+ // The member list is written inline at nearly every call site, so compare
90
+ // the gestures it resolves to rather than the array it arrived in.
91
+ const gestures = useStableList(members.map(memberGesture))
92
+
93
+ const gesture = useMemo(() => compose(mode, gestures), [mode, gestures])
94
+
95
+ return useMemo(() => ({ gesture }), [gesture])
96
+ }
97
+
98
+ /** Unwrap a hook result, or pass a bare gesture through. */
99
+ function memberGesture(member: GestureMember): AttachableGesture {
100
+ return 'gesture' in member ? member.gesture : member
101
+ }
102
+
103
+ function compose(
104
+ mode: ComposeMode,
105
+ gestures: readonly AttachableGesture[],
106
+ ): ComposedGesture {
107
+ if (gestures.length === 0) {
108
+ warnOnce(
109
+ 'compose:empty',
110
+ 'useGestures was given an empty list. The composition recognizes ' +
111
+ 'nothing, so the `<GestureDetector>` holding it is inert. Build the ' +
112
+ 'list conditionally around the hook call rather than passing no ' +
113
+ 'members.',
114
+ )
115
+ }
116
+
117
+ switch (mode) {
118
+ case 'race':
119
+ return Gesture.Race(...gestures)
120
+ case 'simultaneous':
121
+ return Gesture.Simultaneous(...gestures)
122
+ case 'exclusive':
123
+ return Gesture.Exclusive(...gestures)
124
+ default:
125
+ // Unreachable from TypeScript. Reachable from JavaScript, and a
126
+ // misspelled mode would otherwise compose nothing and fail as "the
127
+ // gesture does not work".
128
+ warnOnce(
129
+ `compose:mode:${String(mode)}`,
130
+ `useGestures received an unknown mode ${JSON.stringify(mode)}. ` +
131
+ 'Falling back to `race`. Valid modes are `race`, `simultaneous`, ' +
132
+ 'and `exclusive`.',
133
+ )
134
+ return Gesture.Race(...gestures)
135
+ }
136
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@rootnative/impulse/gesture-handler` — the RNGH interop subpath.
3
+ *
4
+ * Impulse hides RNGH by default, and its intent hooks cover the common cases.
5
+ * For the rest, this subpath re-exports RNGH's own primitives under their
6
+ * original names, so an app that needs a mechanism Impulse does not model
7
+ * still has `@rootnative/impulse` as its only gesture import.
8
+ *
9
+ * These are pure re-exports and carry no Impulse behaviour. `Gesture.Pan()`
10
+ * reached through here is the same object as `Gesture.Pan()` reached from
11
+ * `react-native-gesture-handler`, which means none of Impulse's guarantees —
12
+ * stable gesture identity, thread-explicit callbacks, intent-shaped payloads
13
+ * — apply to what you build with it.
14
+ *
15
+ * `GestureDetector` is not listed here because the root entry already exports
16
+ * it; importing it from both paths would be two names for one thing.
17
+ */
18
+ export * from 'react-native-gesture-handler'
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `@rootnative/impulse` — declarative gesture primitives for React Native.
3
+ *
4
+ * This entry carries the composition and coexistence core: `useGestures` for
5
+ * relating gestures to one another, `useRawGesture` for building a recognizer
6
+ * RNGH models and Impulse does not, and the `alongside` / `blocks` /
7
+ * `deferTo` options both accept.
8
+ *
9
+ * `useTap` and `useDrag` are the two intent hooks, and the pattern the rest
10
+ * follow: an intent-shaped payload, JS-thread callbacks named for what
11
+ * happened, worklet phase callbacks named for when, and documented activation
12
+ * criteria. The remaining intents — `useSwipe`, `usePinch`, and the rest — are
13
+ * designed but not implemented; see the repository README for the roadmap.
14
+ */
15
+
16
+ // Re-exported because every hook's result has to be handed to it, and
17
+ // reaching for it is not a reason to add a second gesture import to an app.
18
+ // The rest of RNGH stays behind `@rootnative/impulse/gesture-handler`.
19
+ export { GestureDetector } from 'react-native-gesture-handler'
20
+
21
+ export { useGestures } from './compose'
22
+ export type {
23
+ GestureCarrier,
24
+ GestureMember,
25
+ GesturesResult,
26
+ UseGesturesOptions,
27
+ } from './compose'
28
+
29
+ export { useRawGesture } from './raw'
30
+ export type { RawGestureResult, UseRawGestureOptions } from './raw'
31
+
32
+ export { useTap } from './intents/tap'
33
+ export type { TapEvent, UseTapOptions, UseTapResult } from './intents/tap'
34
+
35
+ export { useDrag } from './intents/drag'
36
+ export type {
37
+ DragAxis,
38
+ DragBounds,
39
+ DragEvent,
40
+ UseDragOptions,
41
+ UseDragResult,
42
+ } from './intents/drag'
43
+
44
+ export type {
45
+ AttachableGesture,
46
+ ComposeMode,
47
+ CoexistenceOptions,
48
+ GestureReference,
49
+ GestureReferences,
50
+ HitSlop,
51
+ IntentResult,
52
+ Point,
53
+ } from './types'
@@ -0,0 +1,8 @@
1
+ export { useDrag } from '../useDrag'
2
+ export type {
3
+ DragAxis,
4
+ DragBounds,
5
+ DragEvent,
6
+ UseDragOptions,
7
+ UseDragResult,
8
+ } from '../useDrag'
@@ -0,0 +1,2 @@
1
+ export { useTap } from '../useTap'
2
+ export type { TapEvent, UseTapOptions, UseTapResult } from '../useTap'