react-native-gtkx 0.4.0-alpha.2 → 0.4.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 (63) hide show
  1. package/README.md +35 -96
  2. package/dist/adw/index.js +3 -2
  3. package/dist/adw/index.js.map +1 -1
  4. package/dist/apis/host.gtkx.js.map +1 -1
  5. package/dist/components/modal.d.ts +1 -1
  6. package/dist/components/modal.js +16 -8
  7. package/dist/components/modal.js.map +1 -1
  8. package/dist/dnd/gtk-controllers.js +1 -1
  9. package/dist/dnd/gtk-controllers.js.map +1 -1
  10. package/dist/gtk/index.js +4 -3
  11. package/dist/gtk/index.js.map +1 -1
  12. package/dist/gtkx/bridge/adw.js +1 -1
  13. package/dist/gtkx/bridge/adw.js.map +1 -1
  14. package/dist/gtkx/bridge/core.d.ts +1 -2
  15. package/dist/gtkx/bridge/core.js +2 -5
  16. package/dist/gtkx/bridge/core.js.map +1 -1
  17. package/dist/gtkx/bridge/geometry.js +1 -1
  18. package/dist/gtkx/bridge/geometry.js.map +1 -1
  19. package/dist/gtkx/bridge/layout-manager.js +18 -5
  20. package/dist/gtkx/bridge/layout-manager.js.map +1 -1
  21. package/dist/gtkx/bridge/slot-portal.js.map +1 -1
  22. package/dist/gtkx/bridge/svg-node.js +13 -6
  23. package/dist/gtkx/bridge/svg-node.js.map +1 -1
  24. package/dist/gtkx/bridge/view-box.js +19 -10
  25. package/dist/gtkx/bridge/view-box.js.map +1 -1
  26. package/dist/gtkx/bridge/widgets.generated.adw.js +1 -1
  27. package/dist/gtkx/bridge/widgets.generated.adw.js.map +1 -1
  28. package/dist/mcp/bin.js +1 -1
  29. package/dist/mcp/bin.js.map +1 -1
  30. package/dist/mcp/data/generated.d.ts +860 -531
  31. package/dist/mcp/data/generated.js +952 -538
  32. package/dist/mcp/data/generated.js.map +1 -1
  33. package/dist/mcp/resolve.d.ts +2 -2
  34. package/dist/mcp/resolve.js +2 -2
  35. package/dist/mcp/resolve.js.map +1 -1
  36. package/dist/mcp/server.js +2 -2
  37. package/dist/mcp/server.js.map +1 -1
  38. package/dist/runner/host-dev.js +5 -0
  39. package/dist/runner/host-dev.js.map +1 -1
  40. package/dist/runner/host.js +15 -0
  41. package/dist/runner/host.js.map +1 -1
  42. package/dist/runner/index.d.ts +27 -0
  43. package/dist/runner/index.js +206 -1
  44. package/dist/runner/index.js.map +1 -1
  45. package/dist/sea/assemble.d.ts +9 -0
  46. package/dist/sea/assemble.js +16 -3
  47. package/dist/sea/assemble.js.map +1 -1
  48. package/dist/sea/bundle.d.ts +37 -2
  49. package/dist/sea/bundle.js +77 -5
  50. package/dist/sea/bundle.js.map +1 -1
  51. package/dist/sea/native-shim.js +4 -3
  52. package/dist/sea/native-shim.js.map +1 -1
  53. package/dist/testing/index.js +28 -0
  54. package/dist/testing/index.js.map +1 -1
  55. package/dist/vite/index.js +3 -3
  56. package/dist/vite/index.js.map +1 -1
  57. package/dist/vitest/index.d.ts +2 -2
  58. package/dist/vitest/index.js +1 -1
  59. package/dist/vitest/index.js.map +1 -1
  60. package/package.json +9 -9
  61. package/dist/gtkx/bridge/use-signal.d.ts +0 -2
  62. package/dist/gtkx/bridge/use-signal.js +0 -41
  63. package/dist/gtkx/bridge/use-signal.js.map +0 -1
@@ -1,7 +1,9 @@
1
1
  // GENERATED FILE — do not edit by hand.
2
- // Produced by scripts/generate-mcp-data.mjs from docs/api.md,
3
- // docs/platform-layer.md, docs/gtkx-rc4-notes.md, docs/getting-started.md,
4
- // docs/gestures.md, docs/research/navigation-extensibility.md and
2
+ // Produced by scripts/generate-mcp-data.mjs from docs/reference/*.md
3
+ // (including docs/reference/components/*.md, one page per component),
4
+ // docs/architecture/*.md, docs/guide/*.md, docs/gtkx-1.2-notes.md,
5
+ // docs/getting-started.md,
6
+ // docs/research/navigation-extensibility.md and
5
7
  // scripts/widget-surface/classification.json.
6
8
  //
7
9
  // Regenerate after touching any of those:
@@ -12,289 +14,276 @@ export const PORTABLE_COMPONENTS = [
12
14
  {
13
15
  name: "View",
14
16
  subpath: "react-native",
15
- gtkImplementation: "GtkBox (RnGtkxViewBox)",
16
- supported: "`style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), `focusable` + `onFocus`/`onBlur` (RN has `focusable` on View for Android/Windows; react-native-web and react-native-windows both have the callbacks — off by default, as in RN), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props ([guide](gestures.md)) (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here",
17
- differences: "Responder negotiation is RN's in full — capture-then-bubble, transfer to an ancestor mid-gesture through `onResponderTerminationRequest`/`onResponderReject`, and `onResponderTerminate`. The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one. Terminations differ from RN's, because GTK decides most of them before JS is told: a context menu (a second mouse button), a native widget or a `Controllers` `GtkDragSource` taking the sequence, and text selection all arrive as a cancelled gesture and terminate **without** consulting `onResponderTerminationRequest` — GTK's `CLAIMED` is irrevocable, so there is nothing an answer could change. Window blur terminates unconditionally, as it does in react-native-web. An enclosing `ScrollView` scrolling under the gesture is the one termination the holder may refuse. `overflow: \"hidden\"` (and `\"scroll\"`, which clips identically — a `View` is not made scrollable by a style on this platform any more than it is in RN) clips both the paint AND the picking of the children, including transformed ones and children driven out by an animated absolute `top`/`left`: GTK pushes a clip node over the container's CSS padding box before it snapshots them, and `gtk_widget_pick()` refuses the same box, so what you cannot see you cannot click. `borderRadius` shapes that clip — a rounded container clips its children to the rounded corners. A container never clips its OWN background, border, shadow or outline, only its children's",
17
+ gtkImplementation: "`GtkBox` (a custom paintable box)",
18
+ supported: "`style`, `onLayout`, `testID`, children. `pointerEvents` — `auto` / `none` / `box-none` / `box-only`, mapped onto GTK picking (a can-target flag plus a `contains()` override). Also honored from `style.pointerEvents`, with the prop taking precedence. `focusable` plus `onFocus` / `onBlur` — off by default, as in RN. A ref exposing `measure` / `measureInWindow` / `measureLayout` (`ViewHandle`, RN's own argument order window coordinates come from `gtk_widget_compute_point`, so they read correctly inside a scrolled viewport). The full responder and touch prop set `onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` plus `Capture`; `PanResponder`'s `panHandlers` spread here too. See [Gestures](../../architecture/gestures.md).",
19
+ differences: 'Input is single-pointer: a mouse is one fabricated touch, and `touches` never exceeds one. Responder negotiation is RN\'s model in full — capture-then-bubble, mid-gesture transfer through `onResponderTerminationRequest` / `onResponderReject`, one lock per process but the negotiation path stops at the layout root, so native GTK widgets between or above views take no part in it. GTK settles most terminations before JS is consulted: a context menu, a native widget or `GtkDragSource` taking the sequence, and text selection all arrive as an already-cancelled gesture and terminate **without** consulting `onResponderTerminationRequest` — GTK\'s claim is irrevocable. Window blur terminates unconditionally (as on react-native-web). An enclosing `ScrollView` scrolling under the gesture is the one termination the responder may still refuse. `overflow: "hidden"` (and `"scroll"`, which clips identically) clips both the paint and the picking of children including transformed ones and children an animation drives outside the box. `borderRadius` shapes that clip. A container never clips its own background, border, shadow or outline only its children\'s.',
18
20
  },
19
21
  {
20
22
  name: "Text",
21
23
  subpath: "react-native",
22
- gtkImplementation: "GtkLabel (Pango)",
23
- supported: "wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID`, ref: `measure`/`measureInWindow`/`measureLayout` (`TextHandle` RN gives every host component the geometry methods, so a label no longer has to be wrapped in a `View` to be measurable)",
24
- differences: "nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows)",
24
+ gtkImplementation: "`GtkLabel` (Pango)",
25
+ supported: "wrapping, `numberOfLines` (end ellipsis), `textAlign`, font styles, `onLayout`, `testID`, and a ref exposing the geometry methods (`TextHandle` a label needs no wrapping `View` to be measurable).",
26
+ differences: "Nested `Text` elements are concatenated without per-span styling. Text is always ellipsizable it shrinks in a narrow window rather than overflowing.",
25
27
  },
26
28
  {
27
29
  name: "Image",
28
30
  subpath: "react-native",
29
- gtkImplementation: "GtkPicture",
30
- supported: '`source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the "Svg" section below — a separate import, not part of this table); ref: `measure`/`measureInWindow`/`measureLayout` (`ImageHandle`)',
31
- differences: "no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet",
31
+ gtkImplementation: "`GtkPicture`",
32
+ supported: "`source={{ uri }}` or a string — local paths, `file://` and `http(s)` (fetched through Node and cached to disk by URL, with in-flight requests de-duplicated). `resizeMode` — `cover` / `contain` / `stretch` / `center`. `onLoad` / `onError`; a ref exposing the geometry methods (`ImageHandle`). `.svg` files load like any other image (rasterized through librsvg). Building vector graphics from state instead of a file is a separate import see [Svg](../svg.md).",
33
+ differences: "A remote image has no synchronous size `style` sets the size, as in RN. The disk cache is not size-limited yet.",
32
34
  },
33
35
  {
34
- name: "TextInput",
35
- subpath: "react-native",
36
- gtkImplementation: "GtkEntry / GtkTextView",
37
- supported: "controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only), the visual half of `style` (background, border, radius — it used to be computed and dropped, so a styled TextInput silently kept the theme's own frame) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics)",
38
- differences: "multiline needs a height in the style (as RN recommends)",
39
- },
40
- {
41
- name: "Pressable",
36
+ name: "SafeAreaView",
42
37
  subpath: "react-native",
43
- gtkImplementation: "GtkBox (RnGtkxViewBox) + GestureClick/Motion",
44
- supported: "`onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `onFocus`/`onBlur`, `focusable`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered, focused}` (react-native-web's own state shape); **keyboard-operable**: `focusable` defaults to true when `onPress` is set (react-native-web's rule), which puts the view in GTK's focus chain so Tab and the arrow keys reach it, and Enter/Space fire `onPress` as they do on web and Android; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` a desktop pointer is one fabricated touch). `hitSlop` and `pressRetentionOffset`, each a number or per-edge; the press rect defaults to RN's own `{top: 20, left: 20, right: 20, bottom: 30}` around the hit rect, and a release outside it is a cancel rather than a press",
45
- differences: '`hitSlop` cannot escape an ancestor that clips — a `ScrollView` viewport, or any view with `overflow: "hidden"` because GTK stops picking at the clip, which is the limit RN documents on Android for the same reason. Hover fires from touch as well as from a mouse: react-native-web filters that out, and here a GTK crossing event carries no device to filter on; GTK also sends a matching leave when a touch sequence ends, so the stuck phantom hover the filter exists for does not arise, and GTK\'s own `:hover` behaves the same way (docs/research/gestures.md)',
38
+ gtkImplementation: "`View`",
39
+ supported: "—",
40
+ differences: "A desktop window has no notch to inset around, so this renders exactly as `View`, with no safe-area insets applied.",
46
41
  },
47
42
  {
48
- name: "TouchableOpacity",
43
+ name: "StatusBar",
49
44
  subpath: "react-native",
50
- gtkImplementation: "on top of Pressable",
51
- supported: "`activeOpacity`",
52
- differences: "",
45
+ gtkImplementation: "renders nothing",
46
+ supported: "",
47
+ differences: "There is no status bar on a desktop window, so every prop is accepted and ignored.",
53
48
  },
54
49
  {
55
- name: "TouchableHighlight",
50
+ name: "ActivityIndicator",
56
51
  subpath: "react-native",
57
- gtkImplementation: "on top of Pressable",
58
- supported: "`underlayColor` (RN default `black`), `activeOpacity`, `onShowUnderlay`/`onHideUnderlay`",
59
- differences: "RN renders a separate underlay VIEW behind the child and dims the child onto it; here the highlight is the view's own `backgroundColor` while pressed, because an extra box would change flex layout and what `measureLayout` is relative to — the same reason `GestureDetector` and `createAnimatedComponent` add none. Give the child a translucent background for RN's exact blend.",
52
+ gtkImplementation: "`GtkSpinner`",
53
+ supported: "`animating`, `size` (`small` / `large` / a number).",
54
+ differences: "`color` is not supported yet.",
60
55
  },
61
56
  {
62
- name: "TouchableWithoutFeedback",
57
+ name: "Root",
63
58
  subpath: "react-native",
64
- gtkImplementation: "on top of Pressable",
65
- supported: "the `Pressable` press/hover/focus props, with no visual reaction",
66
- differences: "RN clones its single child instead of rendering a box, which its own docs call a mistake kept for compatibility; this renders the `Pressable` box. Prefer `Pressable`, as RN's docs say.",
59
+ gtkImplementation: "an internal layout root",
60
+ supported: "`width`, `height`.",
61
+ differences: "Extension: the root the test harness renders a tree into.",
67
62
  },
68
63
  {
69
- name: "ScrollView",
64
+ name: "NestedRoot",
70
65
  subpath: "react-native",
71
- gtkImplementation: "GtkScrolledWindow",
72
- supported: "vertical/`horizontal`, `contentContainerStyle` (RN's default: the content container is a plain `View`, so `alignItems` is `stretch` children fill the cross axis unless they say otherwise), `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onScrollBeginDrag`/`onScrollEndDrag`/`onMomentumScrollBegin`/`onMomentumScrollEnd` (the four phases, each carrying the same `ScrollEvent` — see the Differences column), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` **plus the geometry methods** `measure`/`measureInWindow`/`measureLayout` (`ScrollViewHandle`). A view inside it that takes the responder suspends the scroller's own gestures for the rest of the interaction — RN's `setIsJSResponder`, so a child pan is reachable inside a scrolling list",
73
- differences: "`animated` in scrollTo is ignored. **The scroll phases are input-device-aware**: GTK gives a mouse wheel isolated detents, so this platform groups a burst into one desktop begin/end session (120 ms idle boundary) and reports no momentum; a touchpad glide reports all four from its native GTK sequence, and the content really does keep moving after the fingers leave. RN has no wheel, so the wheel session is an explicit desktop extension rather than a parity claim. `onScrollBeginDrag`/`onScrollEndDrag` map onto the user-driven scroll SESSION (GTK's `::scroll-begin`/`::scroll-end` for touchpad, the grouped burst for wheel) rather than onto a finger on the content, which is the one approximation: a touchpad never touches the content, so \"the user started driving this scroller\" is the closest true statement. The momentum pair is read off the adjustment actually moving after the sequence ended, not off `::decelerate` (which fires at every lift, velocity or none), so a glide that ends dead reports the drag pair and no momentum pair — as RN does. **None of it is installed until a handler is attached**: no controller, no signal, no timer, and 6.93 µs per scroll event with all four attached against 7.17 µs with none, which is inside the noise; the GTK-controller residual is 0.31 µs per event while any phase handler is attached. A begin/end consumer also makes each wheel detent enter the 0.235 µs session state machine; momentum-only and untracked scrollers do not. Traces and numbers: [research/scroll-phases.md](research/scroll-phases.md). Scroll arbitration is **touch-only and unverified end to end**: all four gestures `GtkScrolledWindow` installs are touch-only, so under a mouse a child pan never competes with scrolling at all, and no touch can be injected on the test rig (wlroots has no virtual-touch protocol) — every link of the mechanism is tested, the finger is not. Two known edges on touch: a view that claims on a MOVE rather than on press can lose the first ~8 px to the scroller, which `CLAIMED` makes irrevocable (iOS has the same artefact); and the mouse wheel is deliberately left alone, so scrolling with a wheel during a gesture terminates the responder rather than being suppressed. **The scroller carries RN's own base style, `flexGrow: 1, flexShrink: 1`** `styles.baseVertical`/`baseHorizontal`, composed UNDER the app's `style` exactly as RN's `StyleSheet.compose(baseStyle, this.props.style)` composes it, on the same node the `style` prop lands on. `FlatList`, `SectionList` and `VirtualizedList` inherit it. It is what makes a scrollable with NO style of its own a viewport rather than a box grown to its content: libraries hand their list down unstyled and let the host size it (`@gorhom/bottom-sheet` is the one that found this — its scroll lock had nothing to lock, because a list that never became a viewport never emitted a scroll event). **The surprise is the other consequence**: an explicit main-axis `height` on the scroller is only its flex BASIS, so in a taller flex parent grow expands past it — `<ScrollView style={{ height: 200 }}>` inside a 400px column lays out at 400, not 200. That is parity, not a deviation, and it was checked rather than assumed: RN 0.86.2 composes the same two properties under the same `style` prop on the same Yoga node, and in Yoga a definite main-axis length becomes the flex BASIS (`computeFlexBasisForChild`) before `resolveFlexibleLength` hands out the free space — neither function consults any config. The one Yoga setting the two platforms differ on is errata (RN runs the tree at `YGErrataAll`, this platform at Yoga's default `None`) and it is inert here: the only flex-relevant bit, `StretchFlexBasis`, is reached only when the CONTAINER's main size is indefinite, and a bounded parent's is not. `useWebDefaults` is false on both, which is exactly why the `flexShrink: 1` half is needed at all. To bound the viewport, bound the PARENT (`<View style={{ height: 200 }}><FlatList /></View>` — what an RN app writes) or cancel the base with `flexGrow: 0`",
66
+ gtkImplementation: "an internal layout root",
67
+ supported: "—",
68
+ differences: "Extension: a Yoga layout root inside any GTK container slot (a navigation page, a custom container) — the slot's own allocation is the viewport.",
74
69
  },
75
70
  {
76
- name: "FlatList",
71
+ name: "IntrinsicRoot",
77
72
  subpath: "react-native",
78
- gtkImplementation: "windowed core on ScrollView",
79
- supported: "virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `CellRendererComponent` (RN's per-cell wrapper — the list still hands it the cell's absolute `style` and the `onLayout` that measures it, and both must be applied; this is what `react-native-draggable-flatlist` builds its whole design on), `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + `scrollTo`/`scrollToEnd` (`FlatListHandle`) — the SCROLL half of a ScrollView ref, not the geometry half: a windowed list is a composite over a ScrollView and owns no widget of its own, so a `measure()` here would have to pick some inner widget and pretend it was the list. Measure the `ScrollView` or a cell",
80
- differences: "1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom; `CellRendererComponent` is not applied to a STICKY cell (`stickyHeaderIndices`) — pinning reorders the cell's real GTK widget, so the sticky container has to BE the cell",
73
+ gtkImplementation: "an internal layout root",
74
+ supported: "—",
75
+ differences: "Extension: a content-sized Yoga root for chrome slots (a header bar's start/end content) it reports its content size to GTK instead of receiving an allocation.",
81
76
  },
82
77
  {
83
- name: "SectionList",
78
+ name: "TextInput",
84
79
  subpath: "react-native",
85
- gtkImplementation: "on top of FlatList",
86
- supported: "`sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`)",
87
- differences: "viewability props are not exposed (section-aware ViewTokens pending)",
80
+ gtkImplementation: "`GtkEntry` (single line) / `GtkTextView` (multiline)",
81
+ supported: "Controlled and uncontrolled use (`value` / `defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus` / `onBlur`. `placeholder` its own dim overlay in multiline mode, since `GtkTextView` has none built in. `secureTextEntry`, `editable`, `keyboardType`, `multiline`. `clearButtonMode` — `GtkEntry`'s built-in clear icon (RN only ships this on iOS). The visual half of `style` — background, border and radius all reach the widget, rather than being computed and dropped.",
82
+ differences: "Multiline needs an explicit `height` in its style, exactly as RN recommends. A real `GtkTextView` wraps words, scrolls internally, and inserts a newline on Enter rather than firing `onSubmitEditing` — RN's own multiline semantics.",
88
83
  },
89
84
  {
90
- name: "VirtualizedList",
85
+ name: "Switch",
91
86
  subpath: "react-native",
92
- gtkImplementation: "the same windowed core",
93
- supported: "RN's data-source shape over the list `FlatList` already sits on: `data` is OPAQUE and read only through `getItemCount(data)` and `getItem(data, index)`, both honoured LAZILY (only the rows the window mounts are ever asked for). Everything else is `FlatList`'s row above, `CellRendererComponent` included",
94
- differences: "The accessors are OPTIONAL here and required upstream — one component serves both shapes, which is why `FlatList` needs no separate implementation. `scrollToItem` scans the source through `getItem` (upstream scans too; an opaque source has no index to ask). Every difference in the `FlatList` row applies unchanged",
87
+ gtkImplementation: "`GtkSwitch`",
88
+ supported: "`value` / `onValueChange`, `disabled`.",
89
+ differences: "Sized by the GTK theme, not by iOS metrics.",
95
90
  },
96
91
  {
97
- name: "Switch",
92
+ name: "Pressable",
98
93
  subpath: "react-native",
99
- gtkImplementation: "GtkSwitch",
100
- supported: "`value`/`onValueChange`, `disabled`",
101
- differences: "sized by the GTK theme, not iOS metrics",
94
+ gtkImplementation: "`View` + click/motion event controllers",
95
+ supported: "`onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn` / `onHoverOut`, `onFocus` / `onBlur`, `focusable`, `disabled`. A function-form `style` / `children` receiving `{ pressed, hovered, focused }` (react-native-web's own state shape). Keyboard-operable: `focusable` defaults to `true` whenever `onPress` is set (react-native-web's rule), which puts the view in the GTK focus chain — Tab and the arrow keys reach it, and Enter/Space fire `onPress` as they do on web and Android. The `PressEvent` payload matches RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, a monotonic `timestamp`, single-element `touches`/`changedTouches`). `hitSlop` and `pressRetentionOffset` each take a number or a per-edge object; the press rect defaults to RN's own `{ top: 20, left: 20, right: 20, bottom: 30 }` around the hit rect, and releasing outside it cancels rather than presses.",
96
+ differences: '`hitSlop` cannot escape a clipping ancestor — a `ScrollView` viewport or any view with `overflow: "hidden"` because GTK stops hit-testing at the clip; RN documents the identical limit on Android for the same reason. Hover fires from touch input as well as from a mouse (react-native-web filters that out; here a crossing event carries no device to filter on) — GTK also sends a matching leave when a touch sequence ends, so the stuck phantom hover the filter guards against does not arise; GTK\'s own `:hover` behaves the same way.',
102
97
  },
103
98
  {
104
- name: "ActivityIndicator",
99
+ name: "TouchableOpacity",
105
100
  subpath: "react-native",
106
- gtkImplementation: "GtkSpinner",
107
- supported: "`animating`, `size` (small/large/number)",
108
- differences: "no `color` yet",
101
+ gtkImplementation: "built on `Pressable`",
102
+ supported: "`activeOpacity`.",
103
+ differences: "",
109
104
  },
110
105
  {
111
- name: "Modal",
106
+ name: "TouchableHighlight",
112
107
  subpath: "react-native",
113
- gtkImplementation: "modal GtkWindow (portal)",
114
- supported: "`visible`, `onRequestClose` (Escape/close button), `title`, `width`/`height`; independently resizable with relayout",
115
- differences: "desktop semantics: a separate window, not an overlay; `transparent`/`animationType` are no-ops",
108
+ gtkImplementation: "built on `Pressable`",
109
+ supported: "`underlayColor` (default `black`, as in RN), `activeOpacity`, `onShowUnderlay` / `onHideUnderlay`.",
110
+ differences: "RN renders a separate underlay view behind the child and dims the child onto it. Here the highlight is the view's own `backgroundColor` while pressed — an extra box would change flex layout and what `measureLayout` measures relative to, the same reason `GestureDetector` and `createAnimatedComponent` add none either. Give the child a translucent background for RN's exact blend.",
116
111
  },
117
112
  {
118
- name: "Animated.View",
113
+ name: "TouchableWithoutFeedback",
119
114
  subpath: "react-native",
120
- gtkImplementation: "direct widget calls",
121
- supported: "`opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange); `top`/`left`/`right`/`bottom` too when the node's own `position` is `\"absolute\"`, which is what makes `Animated.ValueXY`'s `getLayout()` work; and `width`/`height` where the change is confined to the node that owns it; plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands; plus `pointerEvents` and `animatedProps`, because Reanimated's `Animated.View` is `createAnimatedComponent(View)` and every View prop reaches it there",
122
- differences: "`rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`)",
115
+ gtkImplementation: "built on `Pressable`",
116
+ supported: "the same press/hover/focus props as `Pressable`, with no visual reaction.",
117
+ differences: "RN clones its single child rather than rendering a box of its own its own documentation calls that a compatibility artifact. This renders the `Pressable` box instead. Prefer `Pressable` directly, as RN's own docs recommend.",
123
118
  },
124
119
  {
125
- name: "SafeAreaView",
120
+ name: "ScrollView",
126
121
  subpath: "react-native",
127
- gtkImplementation: "= View",
128
- supported: "—",
129
- differences: "no notches on desktop",
122
+ gtkImplementation: "`GtkScrolledWindow`",
123
+ supported: "Vertical and `horizontal` scrolling. `contentContainerStyle` the content container is a plain `View`, so `alignItems` defaults to `stretch` as it does in RN. `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), the four scroll-phase callbacks `onScrollBeginDrag`/`onScrollEndDrag`/ `onMomentumScrollBegin`/`onMomentumScrollEnd`, `onContentSizeChange`. `stickyHeaderIndices` — the real child is translated and painted on top, no duplicate node. A ref exposing `scrollTo`/`scrollToEnd` plus the geometry methods `measure`/`measureInWindow`/`measureLayout` (`ScrollViewHandle`). A child that takes the responder suspends the scroller's own gestures for the rest of the interaction, so a pan gesture is reachable inside a scrolling list.",
124
+ differences: "`animated` in `scrollTo` is ignored. **The scroll phases are input-device aware**: a mouse wheel gives GTK isolated detents, so a burst is grouped into one begin/end session (a 120&nbsp;ms idle boundary) and reports no momentum; a touchpad glide reports all four phases from its native GTK sequence, and content really keeps moving once the fingers lift. RN has no wheel input, so the wheel session is a desktop-only extension rather than a parity claim. `onScrollBeginDrag`/`onScrollEndDrag` map onto the user-driven scroll _session_ (a touchpad's begin/end signal, or the grouped wheel burst) rather than a finger literally touching the content — the closest true statement available, since a touchpad never touches the content directly. The momentum pair reflects the adjustment actually continuing to move after the session ends rather than a generic \"decelerate\" signal that fires on every lift — a glide that stops dead reports the drag pair with no momentum pair, as RN does. None of this installs until a handler is attached: with all four phase callbacks attached, a scroll event costs 6.93&nbsp;µs versus 7.17&nbsp;µs with none attached — inside the noise; the GTK controller itself costs 0.31&nbsp;µs per event once any phase handler is present, and a begin/end consumer specifically adds 0.235&nbsp;µs per wheel detent for the session state machine. Scroll arbitration between a scroller and a child gesture is touch-only: `GtkScrolledWindow`'s own gestures are touch-only, so under a mouse a child pan never competes with scrolling at all. Two known edges under touch: a child gesture that claims on a move rather than on the initial press can lose the first ~8&nbsp;px to the scroller (GTK's claim is irrevocable, the same artifact iOS has); and a mouse wheel during an active gesture terminates the responder rather than being suppressed. **The scroller carries RN's own base style**, `flexGrow: 1, flexShrink: 1`, composed under the app's `style` the same way RN's `StyleSheet.compose` composes it, on the same node `style` lands on — `FlatList`, `SectionList` and `VirtualizedList` inherit it. This is what makes an unstyled scrollable a viewport rather than a box grown to its content, and it has one consequence worth knowing: an explicit main-axis `height` on the scroller is only its flex _basis_ — inside a taller flex parent, `flexGrow` still expands it past that height. That is parity with RN's own Yoga behavior, not a deviation. To bound the viewport, bound the _parent_ (`<View style={{ height: 200 }}><FlatList /></View>`, what an RN app already writes) or cancel the base style with `flexGrow: 0`.",
130
125
  },
131
126
  {
132
- name: "StatusBar",
127
+ name: "FlatList",
133
128
  subpath: "react-native",
134
- gtkImplementation: "null",
135
- supported: "—",
136
- differences: "no status bar",
129
+ gtkImplementation: "a windowed core over `ScrollView`",
130
+ supported: "Virtualization (`estimatedItemSize` or `getItemLayout`, `windowSize`/ `initialNumToRender` as the primary scroll-performance knobs, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`). `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`. `CellRendererComponent` RN's per-cell wrapper. The list still hands it the cell's absolute `style` and the `onLayout` that measures it, and both must be applied, which is what `react-native-draggable-flatlist` builds its design on. `ListHeader`/`Footer`/`EmptyComponent`, `onEndReached(-Threshold)`. `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`). `inverted` — RN's chat semantics: the list opens at `data[0]` and stays pinned on prepend. `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`. A ref exposing `scrollToIndex`/`scrollToItem`/`scrollToOffset` plus `scrollTo`/`scrollToEnd` (`FlatListHandle`) — the scroll half of a `ScrollView` ref, not the geometry half: a windowed list is a composite over `ScrollView` and owns no widget of its own, so measure the `ScrollView` or a cell instead.",
131
+ differences: "1000 rows mount windowed in roughly 120&nbsp;ms. `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure, and a wider window means fewer mount-and-reflow bursts per scrolled pixel (measured: 21% less churn, late frames down from 10/s to 7.7/s). Rows beyond the visible ones mount `maxToRenderPerBatch` (10) at a time, every `updateCellsBatchingPeriod` (50)&nbsp;ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one. There is no pull gesture — `onRefresh` is always app-triggered. An inverted list shorter than its viewport anchors to the top, not the bottom. `CellRendererComponent` does not apply to a sticky cell (`stickyHeaderIndices`), because pinning reorders the cell's real GTK widget — the sticky container has to _be_ the cell.",
137
132
  },
138
133
  {
139
- name: "Root",
134
+ name: "SectionList",
140
135
  subpath: "react-native",
141
- gtkImplementation: "internal root",
142
- supported: "`width`/`height`",
143
- differences: "extension: required by the test harness",
136
+ gtkImplementation: "built on `FlatList`",
137
+ supported: "`sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`).",
138
+ differences: "Viewability props are not exposed yet (section-aware `ViewToken`s are not implemented).",
144
139
  },
145
140
  {
146
- name: "NestedRoot",
141
+ name: "VirtualizedList",
147
142
  subpath: "react-native",
148
- gtkImplementation: "internal root",
149
- supported: "—",
150
- differences: "extension: a Yoga root inside any GTK container slot (navigation pages, custom containers); the slot allocation is the viewport",
143
+ gtkImplementation: "the same windowed core",
144
+ supported: "RN's opaque data-source shape over the same windowed core `FlatList` sits on `data` is read only through `getItemCount(data)` and `getItem(data, index)`, both called lazily; only the rows the window actually mounts are ever asked for. Everything else matches [FlatList](flat-list.md), `CellRendererComponent` included.",
145
+ differences: "The accessors are optional here and required upstream one component serves both the opaque-source and plain-array shapes, which is why `FlatList` needs no separate implementation. `scrollToItem` scans the source through `getItem`, as upstream does — an opaque source has no index to look up directly. Every `FlatList` difference above applies unchanged.",
151
146
  },
152
147
  {
153
- name: "IntrinsicRoot",
148
+ name: "Modal",
154
149
  subpath: "react-native",
155
- gtkImplementation: "internal root",
156
- supported: "",
157
- differences: "extension: a content-sized Yoga root for chrome slots (HeaderBar start/end) reports its content size to GTK",
150
+ gtkImplementation: "a modal `GtkWindow` (a portal)",
151
+ supported: "`visible`, `onRequestClose` (Escape or the window's close button), `title`, `width`/`height`; independently resizable, with relayout.",
152
+ differences: "This is a real, separate desktop window rather than an overlay drawn above the current one. `transparent` and `animationType` are accepted and have no effect.",
158
153
  },
159
154
  ];
160
155
  export const PORTABLE_APIS = [
161
156
  {
162
157
  name: "StyleSheet",
163
158
  subpath: "react-native",
164
- supported: "`create`, `flatten`, `compose`, `absoluteFill(Object)`, `hairlineWidth`",
159
+ supported: "`create`, `flatten`, `compose`, `absoluteFill`/ `absoluteFillObject`, `hairlineWidth`.",
165
160
  differences: "—",
166
161
  },
167
162
  {
168
163
  name: "PlatformColor",
169
164
  subpath: "react-native",
170
- supported: 'Adwaita variables: `PlatformColor("accent-bg-color")` `var(--...)`, `@named`',
171
- differences: "names are Adwaita, not iOS/Android",
165
+ supported: 'Adwaita theme variables `PlatformColor("accent-bg-color")` resolves to `var(--accent-bg-color)`; `@name` reaches a legacy named GTK color.',
166
+ differences: "The names are Adwaita's own, not iOS's or Android's.",
172
167
  },
173
168
  {
174
169
  name: "AppRegistry",
175
170
  subpath: "react-native",
176
- supported: "`registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,actionAccels,breakpoints,applicationActions,windowActions,windowControllers})`, `getAppKeys`",
177
- differences: 'desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar when this app declares `"Adw-1"` — the app\'s HeaderBars (navigation) become the chrome — and falls back to the plain `GtkApplicationWindow` `chrome: "system"` already uses when it does not (see [Plain GTK profile](#plain-gtk-profile) below): request `chrome: "content"` unconditionally and get HeaderBar-as-chrome where Adw exists, an ordinary window otherwise, with no branching of the app\'s own. `actionAccels` binds accelerators to action names on the `GtkApplication`; `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` WITH `"Adw-1"` declared — otherwise one of two dev warnings fires instead, each at most once per app run: `chrome: "system"` names the wrong chrome, `chrome: "content"` with no `"Adw-1"` names that instead. **`applicationActions`/`windowActions`/`windowControllers` are deprecated** reach for [`<ApplicationActions>`/`<WindowActions>`/`<WindowControllers>`](platform-layer.md#actions-and-shortcuts-declared-in-the-app-tree) instead; they still work unchanged',
171
+ supported: "`registerComponent`, `runApplication(appKey, { title, width, height, initialProps, chrome, actionAccels, breakpoints })`, `getAppKeys`.",
172
+ differences: 'These are desktop window parameters, not mobile ones. `chrome: "content"` uses an `AdwApplicationWindow` with no window titlebar when the app declares `"Adw-1"` in its `gtkx.config.ts` — the app\'s own header bars become the window chrome — and falls back to the plain `GtkApplicationWindow` that `chrome: "system"` always uses when it does not. Requesting `chrome: "content"` unconditionally is the portable choice: header-bar chrome where Adwaita is available, an ordinary window otherwise, with no branch of the app\'s own. `actionAccels` binds accelerators to `GtkApplication` action names; `breakpoints` reaches `AdwApplicationWindow` directly and only takes effect under `chrome: "content"` with `"Adw-1"` declared — otherwise a development warning names the mismatch once per run. `applicationActions`/`windowActions`/`windowControllers` are superseded by the declarative `<ApplicationActions>`/`<WindowActions>`/ `<WindowControllers>` components documented in [Window, navigation, and settings](../architecture/integration.md#actions-and-shortcuts-declared-in-the-tree); they still work unchanged.',
178
173
  },
179
174
  {
180
175
  name: "Platform",
181
176
  subpath: "react-native",
182
- supported: '`OS: "linux"`, `Version` (GTK), `select` (linux → native → default), `isTV`, `isTesting`',
183
- differences: "—",
177
+ supported: '`OS: "linux"`, `Version` (the GTK version), `select` (`linux``native``default`), `isTV`, `isTesting`.',
178
+ differences: '`Platform.OS` is typed as the full `PlatformOSType` union plus `"linux"`, not a `"linux"` literal comparing it against another platform\'s name is a runtime question, and RN\'s own types let that compile everywhere.',
184
179
  },
185
180
  {
186
181
  name: "Dimensions",
187
182
  subpath: "react-native",
188
- supported: '`get("window"/"screen")`, `addEventListener("change")`',
189
- differences: "main window only (transient windows are ignored)",
183
+ supported: '`get("window"/"screen")`, `addEventListener("change")`.',
184
+ differences: "Reports the main window only transient windows are ignored. `get(\"window\")` is the app's own viewport: the window's content area under its header bar, the desktop analogue of RN's app window.",
190
185
  },
191
186
  {
192
187
  name: "useWindowDimensions",
193
188
  subpath: "react-native",
194
- supported: "reactive main-window dimensions",
189
+ supported: "reactive main-window dimensions.",
195
190
  differences: "—",
196
191
  },
197
192
  {
198
193
  name: "Appearance",
199
194
  subpath: "react-native",
200
- supported: "`getColorScheme`, `setColorScheme` (AdwStyleManager; on the plain-GTK profile no `Adw-1` sourced from the `org.freedesktop.appearance` `color-scheme` desktop-portal setting instead, with live updates via its `SettingChanged` signal, falling back to `Gtk.Settings:gtk-application-prefer-dark-theme` when no portal answers), `addChangeListener`",
201
- differences: "On the plain-GTK profile, `setColorScheme` is local-only exactly like the Adw path neither ever writes a SYSTEM-wide preference (`AdwStyleManager` and the fallback `Gtk.Settings` toggle are both per-process only; the portal itself has no write API, by design). With no portal reachable and no explicit `setColorScheme` call yet, the reported scheme is whatever `Gtk.Settings` already defaults to (light) rather than any observed system value",
195
+ supported: "`getColorScheme`, `setColorScheme`, `addChangeListener`. Backed by `AdwStyleManager`; on the plain-GTK profile (no `\"Adw-1\"`), it is sourced from the `org.freedesktop.appearance` desktop portal's `color-scheme` setting instead, with live updates through the portal's own change signal, falling back further to `Gtk.Settings:gtk-application-prefer-dark-theme` when no portal answers.",
196
+ differences: "On every profile, `setColorScheme` writes to this process onlyit never writes a system-wide preference. With no portal reachable and no explicit `setColorScheme` call yet, the reported scheme is whatever `Gtk.Settings` already defaults to (light), not an observed system value.",
202
197
  },
203
198
  {
204
199
  name: "useColorScheme",
205
200
  subpath: "react-native",
206
- supported: "reactive theme",
201
+ supported: "reactive theme.",
207
202
  differences: "—",
208
203
  },
209
204
  {
210
205
  name: "AppState",
211
206
  subpath: "react-native",
212
- supported: "`currentState` active/background, `addEventListener`",
213
- differences: "driven by the window's `is-active`",
207
+ supported: "`currentState` (`active`/`background`), `addEventListener`.",
208
+ differences: "Driven by the window's own active/inactive state.",
214
209
  },
215
210
  {
216
211
  name: "Alert",
217
212
  subpath: "react-native",
218
- supported: "`alert(title, message, buttons, options)` Adw.AlertDialog (`Gtk.AlertDialog`, GTK ≥ 4.10, on the plain-GTK profile no `Adw-1`)",
219
- differences: "`cancel`/`destructive`/`isPreferred` styles. On the plain-GTK profile, `destructive`/`isPreferred` appearance (Adw's `ResponseAppearance`) is lost — `Gtk.AlertDialog` has no equivalent, every button renders the same; default/cancel mapping is preserved. `cancelable: false` with no `cancel`-style button cannot be honored there either: `Gtk.AlertDialog` has no way to block Esc/window-close dismissal the way `Adw.AlertDialog.setCanClose(false)` does — add a `cancel`-style button for identical behavior on both profiles",
213
+ supported: "`alert(title, message, buttons, options)`, backed by `Adw.AlertDialog` (or `Gtk.AlertDialog`, GTK ≥ 4.10, on the plain-GTK profile), including `cancel`/`destructive`/`isPreferred` button styles and default/cancel mapping.",
214
+ differences: "On the plain-GTK profile, `destructive`/`isPreferred` appearance is lost — `Gtk.AlertDialog` has no equivalent, so every button renders the same, though default/cancel mapping is preserved. `cancelable: false` with no `cancel`-style button cannot be enforced there either, since `Gtk.AlertDialog` has no way to block Escape or a window-close dismissal; add a `cancel`-style button for identical behavior on both profiles.",
220
215
  },
221
216
  {
222
217
  name: "Linking",
223
218
  subpath: "react-native",
224
- supported: '`openURL`, `canOpenURL` (http/https/mailto/file), `getInitialURL` (null), `addEventListener("url")`',
225
- differences: 'system launcher; no deep-link delivery on desktop yet — "url" subscriptions never fire',
219
+ supported: '`openURL`, `canOpenURL` (`http`/`https`/`mailto`/`file`), `getInitialURL` (always `null`), `addEventListener("url")`.',
220
+ differences: 'Opens through the system launcher. There is no deep-link delivery on desktop yet — `"url"` subscriptions are accepted but never fire.',
226
221
  },
227
222
  {
228
223
  name: "InteractionManager",
229
224
  subpath: "react-native",
230
- supported: "`runAfterInteractions(task?)` (cancellable, then-able), `createInteractionHandle`/`clearInteractionHandle`, `addListener`",
231
- differences: "navigation transitions register interactions, so screen work deferred with `runAfterInteractions` waits for the push/pop slide",
225
+ supported: "`runAfterInteractions(task?)` (cancellable, then-able), `createInteractionHandle`/`clearInteractionHandle`, `addListener`.",
226
+ differences: "A navigation transition registers itself as an interaction, so work deferred with `runAfterInteractions` during a push or pop waits for the slide to finish.",
232
227
  },
233
228
  {
234
229
  name: "DevSettings",
235
230
  subpath: "react-native",
236
- supported: "`addMenuItem(title, handler)` (entries in the Dev Menu — Ctrl+Shift+D in `run-linux --dev`, the react-native-windows shortcut), `reload(reason?)`",
237
- differences: "silent no-ops in release builds, like RN",
231
+ supported: "`addMenuItem(title, handler)` (entries in the Dev Menu — Ctrl+Shift+D in `run-linux --dev`), `reload(reason?)`.",
232
+ differences: "Silent no-ops in release builds, as in RN.",
238
233
  },
239
234
  {
240
235
  name: "I18nManager",
241
236
  subpath: "react-native",
242
- supported: "`isRTL` (live: GTK's read of the locale text direction), `doLeftAndRightSwapInRTL`, `getConstants`",
243
- differences: "`allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops (mobile persistence has no desktop store)",
237
+ supported: "`isRTL` (a live read of the locale's text direction), `doLeftAndRightSwapInRTL`, `getConstants`.",
238
+ differences: "`allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops mobile's persisted RTL override has no desktop store to persist to.",
244
239
  },
245
240
  {
246
241
  name: "BackHandler",
247
242
  subpath: "react-native",
248
- supported: '`addEventListener("hardwareBackPress")`, `exitApp`',
249
- differences: "no hardware back key on desktop — subscriptions are honored but nothing fires them yet",
243
+ supported: '`addEventListener("hardwareBackPress")`, `exitApp`.',
244
+ differences: "There is no hardware back key on desktop — subscriptions are honored, but nothing fires them yet.",
250
245
  },
251
246
  {
252
247
  name: "findNodeHandle",
253
248
  subpath: "react-native",
254
- supported: "a stable integer per mounted widget, resolvable back to it; accepted by `measureLayout` as its first argument, alongside a handle object. Takes what RN takes a component handle, a node handle (returned unchanged), `null`/`undefined`. A windowed list resolves to the `ScrollView` it renders, as RN's `FlatList` resolves through to its own scroll view",
255
- differences: "The tag identifies the WIDGET, not the ref: two refs onto one view report the same number and a re-render that rebuilt the handle object does not change it. It reaches nothing native there is no `UIManager` to hand a tag to — so it is worth exactly what this platform can resolve it to: `measureLayout`, and identity. `null` for anything that is not a mounted host view (RN's answer too)",
249
+ supported: "a stable integer per mounted widget, resolvable back to it; accepted by `measureLayout` as its first argument, alongside a handle object. Takes what RN takes: a component handle, a node handle (returned unchanged), `null`/`undefined`. A windowed list resolves to the `ScrollView` it renders, as RN's `FlatList` resolves to its own scroll view.",
250
+ differences: "The tag identifies the widget, not the ref: two refs onto one view report the same number, and a re-render that rebuilt the handle object does not change it. It has no native manager to resolve against, so it is worth exactly what this platform can resolve it to `measureLayout`, and identity. `null` for anything that is not a mounted host view, as in RN.",
256
251
  },
257
252
  {
258
253
  name: "Keyboard",
259
254
  subpath: "react-native",
260
- supported: "`addListener` (honoured, never fires), `removeAllListeners`, `dismiss`, `isVisible` (always false), `metrics` (always undefined), `scheduleLayoutAnimation`",
261
- differences: "This is the SOFTWARE keyboard, and a desktop has none: every event it carries describes a panel occluding the app, so none can fire. Subscriptions are real and `remove()` pairs with them (a fake subscription would turn an unmount into a crash) — the same shape as `BackHandler` above, and what react-native-windows inherits from RN core, whose emitter is only ever fed on iOS and Android. **`dismiss()` is a no-op and deliberately not RN's**: RN blurs the focused input as the only way to retract the keyboard, and doing that here would let a library's gesture steal focus from a form",
255
+ supported: "`addListener` (honored, never fires), `removeAllListeners`, `dismiss`, `isVisible` (always `false`), `metrics` (always `undefined`), `scheduleLayoutAnimation`.",
256
+ differences: "Every event this module carries describes a _software_ keyboard occluding the app, and a desktop has none so none of them fire. Subscriptions are real and `remove()` pairs with them, so an unmount never crashes on a stale listener. `dismiss()` is deliberately a no-op rather than RN's own behavior: RN blurs the focused input as its only way to retract the keyboard, and doing that here would let a library's gesture steal focus from a form.",
262
257
  },
263
258
  {
264
259
  name: "LogBox",
265
260
  subpath: "react-native",
266
- supported: "`ignoreLogs`, `ignoreAllLogs`, `install`, `uninstall` — accepted and ignored",
267
- differences: "RN's LogBox is a full-screen dev OVERLAY, and `ignoreLogs` has never filtered the console it keeps a warning out of the yellow box. There is no overlay here, so the console output after the call is already the console output RN would have had, and nothing observable is lost. Called by `react-native-draggable-flatlist` on every `NestableDraggableFlatList` render, to silence a nesting warning this platform does not emit either",
261
+ supported: "`ignoreLogs`, `ignoreAllLogs`, `install`, `uninstall` — accepted and ignored.",
262
+ differences: "RN's LogBox is a full-screen development overlay, and `ignoreLogs` only ever kept a warning out of that overlay it never filtered the console. There is no overlay here, so console output is already what RN's own console output would have been, and nothing observable is lost by calling it.",
268
263
  },
269
264
  {
270
265
  name: "PanResponder",
271
266
  subpath: "react-native",
272
- supported: "`create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native's own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN's `touchHistory` store",
273
- differences: "multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`'s return value is not consumed yet. `onPanResponderTerminationRequest` is asked when an ancestor tries to take the gesture and when an enclosing `ScrollView` scrolls; every other termination is GTK's decision and arrives as `onPanResponderTerminate` unasked (see `View`)",
267
+ supported: "`create(config)` `panHandlers` (spread onto a `View`), the full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — react-native's own `PanResponder.js`, unmodified, running on this platform's own touch-history store.",
268
+ differences: "Multi-touch `gestureState` is single-touch here, since input is one pointer. `onShouldBlockNativeResponder`'s return value is not consulted yet. `onPanResponderTerminationRequest` is asked when an ancestor tries to take the gesture, or when an enclosing `ScrollView` scrolls; every other termination is GTK's own decision and arrives as an unasked `onPanResponderTerminate` (see [View](components/view.md)).",
274
269
  },
275
270
  {
276
271
  name: "Animated",
277
272
  subpath: "react-native",
278
- supported: "`Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity), `ValueXY` (`setValue`/`setOffset`/`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) — the value a `PanResponder` drag writes to; `event(argMapping, config?)` — the arg mapping is positional over the callback's own arguments (an array entry maps that argument, `null` skips it), traversed recursively into plain objects with a leaf that is a `Value`/`ValueXY` written on every call; works attached to `ScrollView`'s `onScroll` (mapping the one event argument) and to `PanResponder` callbacks such as `onPanResponderMove` (mapping `gestureState`, the second argument RN supports mapping either one); `config.listener` is called with the same arguments, after the mapping has run",
279
- differences: "`useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway — and because there is no native side to hand the event to, `Animated.event` always returns the plain JS handler, `useNativeDriver` true or false, rather than sometimes returning the opaque attach/detach object upstream does. A mapped path the real event does not carry is tolerated at any depth (silently left unset) rather than thrown, which is a deliberate widening of RN's own traversal its direct property access throws one level above a missing leaf",
273
+ supported: "![The gallery's Animated section: Animated.timing with looping, Animated.spring overshoot, and Animated.event driving a scroll-linked header.](../shots/gallery/animated.png) ![The gallery's Interpolate section: a multi-stop opacity range, a mirrored extrapolate-clamp bounce, and two interpolations of one Animated.Value.](../shots/gallery/interpolate.png) `Animated` — `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and `deg`/`rad` strings, with clamp/extend/identity extrapolation), `ValueXY` (`setValue`/`setOffset`/ `flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) and `event(argMapping, config?)` reads directly off `PanResponder`'s `gestureState` or a `ScrollView`'s `onScroll`, mapping is positional over the callback's own arguments, traversed recursively into plain objects down to a leaf that is a `Value`/`ValueXY`, and `config.listener` still runs after the mapping does. `Animated.View`'s style takes `opacity` and the whole `transform` array (`translateX`/`translateY`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ`) driven directly by `Animated` nodes rather than through React, plus `top`/`left`/`right`/`bottom` when the node's own `position` is `\"absolute\"` (what makes `ValueXY.getLayout()` work), `width`/`height` where the change is confined to the node that owns it, the same responder and touch props `View` takes, `pointerEvents`, and `animatedProps` because `Animated.View` is `createAnimatedComponent(View)`, and every `View` prop reaches it there.",
274
+ differences: "`rotateX`/`rotateY`/`perspective` (3D transforms), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the component's own center — see [Components](components/index.md#layout-paint-and-hit-testing). `useNativeDriver` is accepted and ignored, with a development warning: the direct path already runs at native speed, and because there is no native side to hand the event to, `Animated.event` always returns the plain JS handler regardless of `useNativeDriver`. A mapped path the real event does not carry is silently left unset at any depth rather than thrown a deliberate widening of RN's own traversal, which throws one level above a missing leaf.",
280
275
  },
281
276
  {
282
277
  name: "Easing",
283
278
  subpath: "react-native",
284
- supported: "linear/ease/quad/cubic/in/out/inOut/bezier",
279
+ supported: "`linear`, `ease`, `quad`, `cubic`, `in`, `out`, `inOut`, `bezier`.",
285
280
  differences: "—",
286
281
  },
287
- {
288
- name: "`requestAnimationFrame`/`cancelAnimationFrame`",
289
- subpath: "react-native",
290
- supported: '**A global, not a module export** — same as in RN itself, which installs both from its own bootstrap (`InitializeCore`) rather than exporting them from `"react-native"`. This platform dropped that bootstrap entirely (there is no mobile environment to polyfill, "the runtime IS Node"), which held for everything except this one browser-ism library code reaches for directly — `react-native-sortables` crashes the whole process at mount without it (docs/research/upstream-libraries.md, "A third experiment"). Installed as a side effect of the package entry (`src/index.ts`), the one module both toolchains load before any app code runs; built on `components/frame-scheduler.ts`\'s `glibScheduler` — the same clock `Animated`/`Reanimated` already share, not a second timer. An id is returned; the callback receives a monotonic, high-resolution timestamp; a callback requested while a batch is running lands on the NEXT frame (never the one currently flushing); cancelling is silent, including for an unknown or already-delivered handle; one callback throwing is reported (`console.error`) and does not stop its siblings in the same batch',
291
- differences: "parity in behavior; not parity in mechanism — there is no native frame on a Linux desktop the way there is on iOS/Android, so this rides the same ~60 fps GLib timer `Animated` does, the way the DOM's `requestAnimationFrame` stands in for it on react-native-web",
292
- },
293
282
  {
294
283
  name: "version",
295
284
  subpath: "react-native",
296
- supported: "package version",
297
- differences: "extension",
285
+ supported: "the package version.",
286
+ differences: "Extension: not part of RN's own API.",
298
287
  },
299
288
  ];
300
289
  export const COMMON_PRIMITIVES = [
@@ -761,689 +750,1114 @@ export const ADW_WIDGETS = [
761
750
  ];
762
751
  export const DOC_CHUNKS = [
763
752
  {
764
- doc: "docs/api.md",
765
- heading: "Components",
766
- text: "| Export | GTK implementation | Supported | Differences from RN |\n| -------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `View` | GtkBox (RnGtkxViewBox) | `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only — mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), `focusable` + `onFocus`/`onBlur` (RN has `focusable` on View for Android/Windows; react-native-web and react-native-windows both have the callbacks — off by default, as in RN), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props ([guide](gestures.md)) (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here | Responder negotiation is RN's in full — capture-then-bubble, transfer to an ancestor mid-gesture through `onResponderTerminationRequest`/`onResponderReject`, and `onResponderTerminate`. The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one. Terminations differ from RN's, because GTK decides most of them before JS is told: a context menu (a second mouse button), a native widget or a `Controllers` `GtkDragSource` taking the sequence, and text selection all arrive as a cancelled gesture and terminate **without** consulting `onResponderTerminationRequest` — GTK's `CLAIMED` is irrevocable, so there is nothing an answer could change. Window blur terminates unconditionally, as it does in react-native-web. An enclosing `ScrollView` scrolling under the gesture is the one termination the holder may refuse. `overflow: \"hidden\"` (and `\"scroll\"`, which clips identically — a `View` is not made scrollable by a style on this platform any more than it is in RN) clips both the paint AND the picking of the children, including transformed ones and children driven out by an animated absolute `top`/`left`: GTK pushes a clip node over the container's CSS padding box before it snapshots them, and `gtk_widget_pick()` refuses the same box, so what you cannot see you cannot click. `borderRadius` shapes that clip — a rounded container clips its children to the rounded corners. A container never clips its OWN background, border, shadow or outline, only its children's |\n| `Text` | GtkLabel (Pango) | wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID`, ref: `measure`/`measureInWindow`/`measureLayout` (`TextHandle` — RN gives every host component the geometry methods, so a label no longer has to be wrapped in a `View` to be measurable) | nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows) |\n| `Image` | GtkPicture | `source={{uri}}`/string — local paths, file:// and **http(s)** (Node fetch → disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the \"Svg\" section below — a separate import, not part of this table); ref: `measure`/`measureInWindow`/`measureLayout` (`ImageHandle`) | no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet |\n| `TextInput` | GtkEntry / GtkTextView | controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline — GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only), the visual half of `style` (background, border, radius — it used to be computed and dropped, so a styled TextInput silently kept the theme's own frame) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing — RN semantics) | multiline needs a height in the style (as RN recommends) |\n| `Pressable` | GtkBox (RnGtkxViewBox) + GestureClick/Motion | `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `onFocus`/`onBlur`, `focusable`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered, focused}` (react-native-web's own state shape); **keyboard-operable**: `focusable` defaults to true when `onPress` is set (react-native-web's rule), which puts the view in GTK's focus chain so Tab and the arrow keys reach it, and Enter/Space fire `onPress` as they do on web and Android; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch). `hitSlop` and `pressRetentionOffset`, each a number or per-edge; the press rect defaults to RN's own `{top: 20, left: 20, right: 20, bottom: 30}` around the hit rect, and a release outside it is a cancel rather than a press | `hitSlop` cannot escape an ancestor that clips — a `ScrollView` viewport, or any view with `overflow: \"hidden\"` — because GTK stops picking at the clip, which is the limit RN documents on Android for the same reason. Hover fires from touch as well as from a mouse: react-native-web filters that out, and here a GTK crossing event carries no device to filter on; GTK also sends a matching leave when a touch sequence ends, so the stuck phantom hover the filter exists for does not arise, and GTK's own `:hover` behaves the same way (docs/research/gestures.md) |\n| `TouchableOpacity` | on top of Pressable | `activeOpacity` | — |\n| `TouchableHighlight` | on top of Pressable | `underlayColor` (RN default `black`), `activeOpacity`, `onShowUnderlay`/`onHideUnderlay` | RN renders a separate underlay VIEW behind the child and dims the child onto it; here the highlight is the view's own `backgroundColor` while pressed, because an extra box would change flex layout and what `measureLayout` is relative to — the same reason `GestureDetector` and `createAnimatedComponent` add none. Give the child a translucent background for RN's exact blend. |\n| `TouchableWithoutFeedback` | on top of Pressable | the `Pressable` press/hover/focus props, with no visual reaction | RN clones its single child instead of rendering a box, which its own docs call a mistake kept for compatibility; this renders the `Pressable` box. Prefer `Pressable`, as RN's docs say. |\n| `ScrollView` | GtkScrolledWindow | vertical/`horizontal`, `contentContainerStyle` (RN's default: the content container is a plain `View`, so `alignItems` is `stretch` — children fill the cross axis unless they say otherwise), `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onScrollBeginDrag`/`onScrollEndDrag`/`onMomentumScrollBegin`/`onMomentumScrollEnd` (the four phases, each carrying the same `ScrollEvent` — see the Differences column), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` **plus the geometry methods** `measure`/`measureInWindow`/`measureLayout` (`ScrollViewHandle`). A view inside it that takes the responder suspends the scroller's own gestures for the rest of the interaction — RN's `setIsJSResponder`, so a child pan is reachable inside a scrolling list | `animated` in scrollTo is ignored. **The scroll phases are input-device-aware**: GTK gives a mouse wheel isolated detents, so this platform groups a burst into one desktop begin/end session (120 ms idle boundary) and reports no momentum; a touchpad glide reports all four from its native GTK sequence, and the content really does keep moving after the fingers leave. RN has no wheel, so the wheel session is an explicit desktop extension rather than a parity claim. `onScrollBeginDrag`/`onScrollEndDrag` map onto the user-driven scroll SESSION (GTK's `::scroll-begin`/`::scroll-end` for touchpad, the grouped burst for wheel) rather than onto a finger on the content, which is the one approximation: a touchpad never touches the content, so \"the user started driving this scroller\" is the closest true statement. The momentum pair is read off the adjustment actually moving after the sequence ended, not off `::decelerate` (which fires at every lift, velocity or none), so a glide that ends dead reports the drag pair and no momentum pair — as RN does. **None of it is installed until a handler is attached**: no controller, no signal, no timer, and 6.93 µs per scroll event with all four attached against 7.17 µs with none, which is inside the noise; the GTK-controller residual is 0.31 µs per event while any phase handler is attached. A begin/end consumer also makes each wheel detent enter the 0.235 µs session state machine; momentum-only and untracked scrollers do not. Traces and numbers: [research/scroll-phases.md](research/scroll-phases.md). Scroll arbitration is **touch-only and unverified end to end**: all four gestures `GtkScrolledWindow` installs are touch-only, so under a mouse a child pan never competes with scrolling at all, and no touch can be injected on the test rig (wlroots has no virtual-touch protocol) — every link of the mechanism is tested, the finger is not. Two known edges on touch: a view that claims on a MOVE rather than on press can lose the first ~8 px to the scroller, which `CLAIMED` makes irrevocable (iOS has the same artefact); and the mouse wheel is deliberately left alone, so scrolling with a wheel during a gesture terminates the responder rather than being suppressed. **The scroller carries RN's own base style, `flexGrow: 1, flexShrink: 1`** — `styles.baseVertical`/`baseHorizontal`, composed UNDER the app's `style` exactly as RN's `StyleSheet.compose(baseStyle, this.props.style)` composes it, on the same node the `style` prop lands on. `FlatList`, `SectionList` and `VirtualizedList` inherit it. It is what makes a scrollable with NO style of its own a viewport rather than a box grown to its content: libraries hand their list down unstyled and let the host size it (`@gorhom/bottom-sheet` is the one that found this — its scroll lock had nothing to lock, because a list that never became a viewport never emitted a scroll event). **The surprise is the other consequence**: an explicit main-axis `height` on the scroller is only its flex BASIS, so in a taller flex parent grow expands past it — `<ScrollView style={{ height: 200 }}>` inside a 400px column lays out at 400, not 200. That is parity, not a deviation, and it was checked rather than assumed: RN 0.86.2 composes the same two properties under the same `style` prop on the same Yoga node, and in Yoga a definite main-axis length becomes the flex BASIS (`computeFlexBasisForChild`) before `resolveFlexibleLength` hands out the free space — neither function consults any config. The one Yoga setting the two platforms differ on is errata (RN runs the tree at `YGErrataAll`, this platform at Yoga's default `None`) and it is inert here: the only flex-relevant bit, `StretchFlexBasis`, is reached only when the CONTAINER's main size is indefinite, and a bounded parent's is not. `useWebDefaults` is false on both, which is exactly why the `flexShrink: 1` half is needed at all. To bound the viewport, bound the PARENT (`<View style={{ height: 200 }}><FlatList /></View>` — what an RN app writes) or cancel the base with `flexGrow: 0` |\n| `FlatList` | windowed core on ScrollView | virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `CellRendererComponent` (RN's per-cell wrapper — the list still hands it the cell's absolute `style` and the `onLayout` that measures it, and both must be applied; this is what `react-native-draggable-flatlist` builds its whole design on), `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + `scrollTo`/`scrollToEnd` (`FlatListHandle`) — the SCROLL half of a ScrollView ref, not the geometry half: a windowed list is a composite over a ScrollView and owns no widget of its own, so a `measure()` here would have to pick some inner widget and pretend it was the list. Measure the `ScrollView` or a cell | 1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s → 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom; `CellRendererComponent` is not applied to a STICKY cell (`stickyHeaderIndices`) — pinning reorders the cell's real GTK widget, so the sticky container has to BE the cell |\n| `SectionList` | on top of FlatList | `sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`) | viewability props are not exposed (section-aware ViewTokens pending) |\n| `VirtualizedList` | the same windowed core | RN's data-source shape over the list `FlatList` already sits on: `data` is OPAQUE and read only through `getItemCount(data)` and `getItem(data, index)`, both honoured LAZILY (only the rows the window mounts are ever asked for). Everything else is `FlatList`'s row above, `CellRendererComponent` included | The accessors are OPTIONAL here and required upstream — one component serves both shapes, which is why `FlatList` needs no separate implementation. `scrollToItem` scans the source through `getItem` (upstream scans too; an opaque source has no index to ask). Every difference in the `FlatList` row applies unchanged |\n| `Switch` | GtkSwitch | `value`/`onValueChange`, `disabled` | sized by the GTK theme, not iOS metrics |\n| `ActivityIndicator` | GtkSpinner | `animating`, `size` (small/large/number) | no `color` yet |\n| `Modal` | modal GtkWindow (portal) | `visible`, `onRequestClose` (Escape/close button), `title`, `width`/`height`; independently resizable with relayout | desktop semantics: a separate window, not an overlay; `transparent`/`animationType` are no-ops |\n| `Animated.View` | direct widget calls | `opacity` and the whole `transform` array — `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange); `top`/`left`/`right`/`bottom` too when the node's own `position` is `\"absolute\"`, which is what makes `Animated.ValueXY`'s `getLayout()` work; and `width`/`height` where the change is confined to the node that owns it; plus the same responder and touch props `View` takes — this is where an idiomatic `PanResponder` drag lands; plus `pointerEvents` and `animatedProps`, because Reanimated's `Animated.View` is `createAnimatedComponent(View)` and every View prop reaches it there | `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`) |\n| `SafeAreaView` | = View | — | no notches on desktop |\n| `StatusBar` | null | — | no status bar |\n| `Root` | internal root | `width`/`height` | extension: required by the test harness |\n| `NestedRoot` | internal root | — | extension: a Yoga root inside any GTK container slot (navigation pages, custom containers); the slot allocation is the viewport |\n| `IntrinsicRoot` | internal root | — | extension: a content-sized Yoga root for chrome slots (HeaderBar start/end) — reports its content size to GTK |",
753
+ doc: "docs/reference/aliases.md",
754
+ heading: "Configuring the aliases",
755
+ text: 'Both presets take an `aliases` option as **deltas keyed by package name**,\nnot a replacement list — anything not mentioned keeps its default. A\nreplacement list that has to be re-stated in full is a list that can quietly\nlose an entry; losing one of these six from a bundler\'s own external-package\nlist is what admits the real, incompatible upstream package into a Linux\nbuild.\n\n```ts\n// vite.config.ts\nimport { reactNativeGtkx } from "react-native-gtkx/vite"\n\nexport default defineConfig({\n plugins: [\n reactNativeGtkx({\n aliases: {\n // false — drop one of ours, so the real upstream package loads\n "react-native-reanimated-dnd": false,\n // string — an exact name or subpath, tail transplanted\n "my-pkg": "my-pkg/linux",\n // { pattern, replace } — only for the rare case where the subpath\n // layouts genuinely differ\n "weird-pkg": { pattern: /^weird-pkg\\/lib\\/(.+)$/, replace: "impl/$1" },\n },\n }),\n ],\n})\n```\n\n```ts\n// metro.config.ts — the same object, the same semantics\nexport default withLinuxPlatform(getDefaultConfig(__dirname), {\n aliases: { "react-native-reanimated-dnd": false },\n})\n```\n\nPrefer the string form: it is anchored to the exact package name, which\nmatters because `react-native-reanimated-dnd` is a lookalike of\n`react-native-reanimated`, and `react-native-worklets-core` is a real,\nunrelated package that looks like `react-native-worklets`. A loose prefix\nrewrite would send either one onto a subpath that does not exist. Reach for\n`{ pattern, replace }` only when a package\'s subpath layout genuinely does\nnot match its target\'s.\n\nBecause the rules are data rather than functions, a preset validates them\nwhen the config loads and reports exactly what is wrong:\n\n- an unknown key paired with `false` — the aliases that exist are named, so a\n typo cannot silently do nothing;\n- an overlapping pattern — two rules claiming one specifier would make\n resolution order-dependent, so this is rejected;\n- an unanchored pattern, or one carrying the `g`/`y` regex flag — the first\n matches inside a longer specifier than intended, the second carries state\n (`lastIndex`) between calls;\n- a target that is not a plain module specifier — a relative or absolute\n path, or one ending in `/`;\n- `react-native` itself — it cannot be dropped or retargeted; the platform\n alias is not one of the six substituted packages.\n\nOn the vite path, the same option also drives `ssr.noExternal`, derived from\nthe table rather than duplicated beside it — every name in the table stays\ninside vite\'s own pipeline, including one that is turned off deliberately: an\nun-aliased package still imports `react-native` at module scope, and that\nimport only reaches the platform alias if Node never resolves the real\npackage first.',
756
+ },
757
+ {
758
+ doc: "docs/reference/aliases.md",
759
+ heading: "The one alias that is a real trade",
760
+ text: "Five of these six substitute an implementation that cannot run on this\nplatform at all. `react-native-reanimated-dnd` is the exception: its real\n2.0.0 release runs on top of this platform's own Reanimated, worklets and\ngesture-handler surfaces, dragged by a real pointer. Choosing between the two\nis a genuine trade, not a workaround:\n\n- **`react-native-gtkx/dnd` (the default)** — GDK animates a paintable of the\n dragged view above every window, with the desktop theme's own drag\n cursors, hit-testing against the real widget tree, and drops into _other_\n applications. The dragged view itself never moves.\n- **the real `react-native-reanimated-dnd`** — the full upstream prop\n surface (`dragAxis`, `dragBoundsRef`, `dropAlignment`,\n `collisionAlgorithm`, and the rest), and the view genuinely moves under the\n pointer. No drag icon, no cross-application drop, and the drag stays\n confined to the app window.\n\nSee [Drag and drop](dnd.md) for what the mirror does and does not carry over\nfrom upstream's own prop surface; the real package, once aliased off, has\nupstream's behavior by definition.",
767
761
  },
768
762
  {
769
- doc: "docs/api.md",
770
- heading: "API modules",
771
- text: '| Export | Supported | Differences |\n| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `StyleSheet` | `create`, `flatten`, `compose`, `absoluteFill(Object)`, `hairlineWidth` | — |\n| `PlatformColor` | Adwaita variables: `PlatformColor("accent-bg-color")` → `var(--...)`, `@named` | names are Adwaita, not iOS/Android |\n| `AppRegistry` | `registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,actionAccels,breakpoints,applicationActions,windowActions,windowControllers})`, `getAppKeys` | desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar when this app declares `"Adw-1"` — the app\'s HeaderBars (navigation) become the chrome — and falls back to the plain `GtkApplicationWindow` `chrome: "system"` already uses when it does not (see [Plain GTK profile](#plain-gtk-profile) below): request `chrome: "content"` unconditionally and get HeaderBar-as-chrome where Adw exists, an ordinary window otherwise, with no branching of the app\'s own. `actionAccels` binds accelerators to action names on the `GtkApplication`; `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` WITH `"Adw-1"` declared — otherwise one of two dev warnings fires instead, each at most once per app run: `chrome: "system"` names the wrong chrome, `chrome: "content"` with no `"Adw-1"` names that instead. **`applicationActions`/`windowActions`/`windowControllers` are deprecated** — reach for [`<ApplicationActions>`/`<WindowActions>`/`<WindowControllers>`](platform-layer.md#actions-and-shortcuts-declared-in-the-app-tree) instead; they still work unchanged |\n| `Platform` | `OS: "linux"`, `Version` (GTK), `select` (linux → native → default), `isTV`, `isTesting` | — |\n| `Dimensions` | `get("window"/"screen")`, `addEventListener("change")` | main window only (transient windows are ignored) |\n| `useWindowDimensions` | reactive main-window dimensions | — |\n| `Appearance` | `getColorScheme`, `setColorScheme` (AdwStyleManager; on the plain-GTK profile — no `Adw-1` — sourced from the `org.freedesktop.appearance` `color-scheme` desktop-portal setting instead, with live updates via its `SettingChanged` signal, falling back to `Gtk.Settings:gtk-application-prefer-dark-theme` when no portal answers), `addChangeListener` | On the plain-GTK profile, `setColorScheme` is local-only exactly like the Adw path — neither ever writes a SYSTEM-wide preference (`AdwStyleManager` and the fallback `Gtk.Settings` toggle are both per-process only; the portal itself has no write API, by design). With no portal reachable and no explicit `setColorScheme` call yet, the reported scheme is whatever `Gtk.Settings` already defaults to (light) rather than any observed system value |\n| `useColorScheme` | reactive theme | — |\n| `AppState` | `currentState` active/background, `addEventListener` | driven by the window\'s `is-active` |\n| `Alert` | `alert(title, message, buttons, options)` → Adw.AlertDialog (`Gtk.AlertDialog`, GTK ≥ 4.10, on the plain-GTK profile — no `Adw-1`) | `cancel`/`destructive`/`isPreferred` styles. On the plain-GTK profile, `destructive`/`isPreferred` appearance (Adw\'s `ResponseAppearance`) is lost — `Gtk.AlertDialog` has no equivalent, every button renders the same; default/cancel mapping is preserved. `cancelable: false` with no `cancel`-style button cannot be honored there either: `Gtk.AlertDialog` has no way to block Esc/window-close dismissal the way `Adw.AlertDialog.setCanClose(false)` does — add a `cancel`-style button for identical behavior on both profiles |\n| `Linking` | `openURL`, `canOpenURL` (http/https/mailto/file), `getInitialURL` (null), `addEventListener("url")` | system launcher; no deep-link delivery on desktop yet — "url" subscriptions never fire |\n| `InteractionManager` | `runAfterInteractions(task?)` (cancellable, then-able), `createInteractionHandle`/`clearInteractionHandle`, `addListener` | navigation transitions register interactions, so screen work deferred with `runAfterInteractions` waits for the push/pop slide |\n| `DevSettings` | `addMenuItem(title, handler)` (entries in the Dev Menu — Ctrl+Shift+D in `run-linux --dev`, the react-native-windows shortcut), `reload(reason?)` | silent no-ops in release builds, like RN |\n| `I18nManager` | `isRTL` (live: GTK\'s read of the locale text direction), `doLeftAndRightSwapInRTL`, `getConstants` | `allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops (mobile persistence has no desktop store) |\n| `BackHandler` | `addEventListener("hardwareBackPress")`, `exitApp` | no hardware back key on desktop — subscriptions are honored but nothing fires them yet |\n| `findNodeHandle` | a stable integer per mounted widget, resolvable back to it; accepted by `measureLayout` as its first argument, alongside a handle object. Takes what RN takes — a component handle, a node handle (returned unchanged), `null`/`undefined`. A windowed list resolves to the `ScrollView` it renders, as RN\'s `FlatList` resolves through to its own scroll view | The tag identifies the WIDGET, not the ref: two refs onto one view report the same number and a re-render that rebuilt the handle object does not change it. It reaches nothing native — there is no `UIManager` to hand a tag to — so it is worth exactly what this platform can resolve it to: `measureLayout`, and identity. `null` for anything that is not a mounted host view (RN\'s answer too) |\n| `Keyboard` | `addListener` (honoured, never fires), `removeAllListeners`, `dismiss`, `isVisible` (always false), `metrics` (always undefined), `scheduleLayoutAnimation` | This is the SOFTWARE keyboard, and a desktop has none: every event it carries describes a panel occluding the app, so none can fire. Subscriptions are real and `remove()` pairs with them (a fake subscription would turn an unmount into a crash) — the same shape as `BackHandler` above, and what react-native-windows inherits from RN core, whose emitter is only ever fed on iOS and Android. **`dismiss()` is a no-op and deliberately not RN\'s**: RN blurs the focused input as the only way to retract the keyboard, and doing that here would let a library\'s gesture steal focus from a form |\n| `LogBox` | `ignoreLogs`, `ignoreAllLogs`, `install`, `uninstall` — accepted and ignored | RN\'s LogBox is a full-screen dev OVERLAY, and `ignoreLogs` has never filtered the console — it keeps a warning out of the yellow box. There is no overlay here, so the console output after the call is already the console output RN would have had, and nothing observable is lost. Called by `react-native-draggable-flatlist` on every `NestableDraggableFlatList` render, to silence a nesting warning this platform does not emit either |\n| `PanResponder` | `create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) — **react-native\'s own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN\'s `touchHistory` store | multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`\'s return value is not consumed yet. `onPanResponderTerminationRequest` is asked when an ancestor tries to take the gesture and when an enclosing `ScrollView` scrolls; every other termination is GTK\'s decision and arrives as `onPanResponderTerminate` unasked (see `View`) |\n| `Animated` | `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity), `ValueXY` (`setValue`/`setOffset`/`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) — the value a `PanResponder` drag writes to; `event(argMapping, config?)` — the arg mapping is positional over the callback\'s own arguments (an array entry maps that argument, `null` skips it), traversed recursively into plain objects with a leaf that is a `Value`/`ValueXY` written on every call; works attached to `ScrollView`\'s `onScroll` (mapping the one event argument) and to `PanResponder` callbacks such as `onPanResponderMove` (mapping `gestureState`, the second argument — RN supports mapping either one); `config.listener` is called with the same arguments, after the mapping has run | `useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway — and because there is no native side to hand the event to, `Animated.event` always returns the plain JS handler, `useNativeDriver` true or false, rather than sometimes returning the opaque attach/detach object upstream does. A mapped path the real event does not carry is tolerated at any depth (silently left unset) rather than thrown, which is a deliberate widening of RN\'s own traversal — its direct property access throws one level above a missing leaf |\n| `Easing` | linear/ease/quad/cubic/in/out/inOut/bezier | — |\n| `requestAnimationFrame`/`cancelAnimationFrame` | **A global, not a module export** — same as in RN itself, which installs both from its own bootstrap (`InitializeCore`) rather than exporting them from `"react-native"`. This platform dropped that bootstrap entirely (there is no mobile environment to polyfill, "the runtime IS Node"), which held for everything except this one browser-ism library code reaches for directly — `react-native-sortables` crashes the whole process at mount without it (docs/research/upstream-libraries.md, "A third experiment"). Installed as a side effect of the package entry (`src/index.ts`), the one module both toolchains load before any app code runs; built on `components/frame-scheduler.ts`\'s `glibScheduler` — the same clock `Animated`/`Reanimated` already share, not a second timer. An id is returned; the callback receives a monotonic, high-resolution timestamp; a callback requested while a batch is running lands on the NEXT frame (never the one currently flushing); cancelling is silent, including for an unknown or already-delivered handle; one callback throwing is reported (`console.error`) and does not stop its siblings in the same batch | parity in behavior; not parity in mechanism — there is no native frame on a Linux desktop the way there is on iOS/Android, so this rides the same ~60 fps GLib timer `Animated` does, the way the DOM\'s `requestAnimationFrame` stands in for it on react-native-web |\n| `version` | package version | extension |\n\n`StyleProp<T>` defaults its type argument, and `ViewStyle`/`TextStyle`/`ImageStyle` are exported as aliases of the one flat style bag this platform has — so `StyleProp<ViewStyle>`, the way ordinary React Native code writes it, compiles here unchanged. `Platform.OS` is typed as the full `PlatformOSType` union (plus `"linux"`), not the `"linux"` literal: comparing it against another platform is a runtime question, and RN\'s own types let that compile everywhere.\n\nStyles (which keys go where and what is unsupported) — [style system table](../packages/react-native-gtkx/src/style/README.md). Includes `boxShadow` (RN 0.76) and `outlineColor`/`outlineOffset`/`outlineStyle`/`outlineWidth` (RN 0.77): both are what Adwaita\'s own theme uses for the `.card`/`.boxed-list` frame and for every focus ring, so they are the difference between a React Native style approximating the platform look and reproducing it — see [research/react-native-first-showcase.md](research/react-native-first-showcase.md).',
763
+ doc: "docs/reference/apis.md",
764
+ heading: "StyleSheet",
765
+ text: "Supported: `create`, `flatten`, `compose`, `absoluteFill`/\n`absoluteFillObject`, `hairlineWidth`.",
772
766
  },
773
767
  {
774
- doc: "docs/api.md",
775
- heading: "Key differences from React Native (summary)",
776
- text: "1. **Desktop, not mobile**: `Modal` is a real window; `runApplication` accepts a title and dimensions; gestures are mouse-driven (hover works, no touch gestures);\n2. **Node.js runtime**: all of npm/Node is available (fs, sqlite, napi) \"native modules\" are written as regular Node modules; RN libraries with iOS/Android code do not work;\n3. **Layout is exactly RN's**: every container runs a custom GtkLayoutManager that obeys only the Yoga engine — GTK widget minimums never leak into the layout, windows shrink freely, and `Dimensions.get(\"window\")` reports the app viewport (the window's content area under the headerbar, like RN's app window);\n4. **Text**: the ellipsis is opt-in via `numberOfLines`, exactly like RN; plain text wraps naturally and an unbreakable word wider than its box clips to it (a text leaf always clips; a container paint-overflows until its style says otherwise — see `overflow` below);\n5. **transform** is paint-only, like RN: `translateX/Y`, `scale`, `scaleX`, `scaleY` and `rotate`/`rotateZ` apply to any component's style (not just `Animated.View`), the array composes left to right as in RN and CSS, and the origin is the view's centre. A transformed child honestly draws past its container over siblings (later siblings stay on top unless a `zIndex` says otherwise — see 10 below) without moving any ancestor, and GTK routes input through the transform, so a rotated view is clickable in its rotated shape — unless the container asks to clip, and `overflow: \"hidden\"` on it cuts the transformed child off at the edge exactly as it cuts off an untransformed one. Rotation and scale reach the widget as the `GskTransform` of its allocation (`docs/research/transforms.md`); 3D (`rotateX`/`rotateY`/`perspective`), `skewX`/`skewY`, `matrix` and `transformOrigin` are not supported;\n6. **Animations never auto-stop**: the desktop \"reduce animations\" hint is not applied automatically (GTK-side animations are kept on to match `Animated`, which runs on its own timers) — honoring reduced motion stays an app-level opt-in, as in RN;\n7. **Lists are windowed like RN's**: FlatList/SectionList mount only the rows around the viewport (prefix-sum offsets, `estimatedItemSize` refined by real measurements or exact `getItemLayout`); sticky headers translate the REAL widget (no duplicate) and `inverted` follows the RN chat contract — `contentOffset` counts from the end where `data[0]` renders. The one RefreshControl compromise: desktop has no pull gesture, so `refreshing`/`onRefresh` are API-compatible but the trigger is app chrome (a button/shortcut);\n8. The package ships compiled (`dist/`: ESM + `.d.ts` alongside, sources embedded in the maps); consumers — Metro (`react-native-gtkx/metro` preset) and vite (preset) — both consume the built output. Requires Node ≥ 24 (the gtkx runtime floor; the run-linux host also relies on `module.registerHooks`).\n9. **`zIndex` orders paint AND picking, per sibling group.** GTK4 has no z-order property, so the container widget does it: it allocates its children in Yoga's order and _snapshots_ them in `zIndex` order, and a widget covered by a higher-painting sibling declines `gtk_widget_pick()` so input lands where the pixels are. Layout is untouched — only the paint pass sorts. RN's rules, checked rather than assumed and each pinned by a test: it applies whatever `position` is (CSS needs a non-`static` `position`; RN does not, and neither does this); equal values keep document order (the sort is stable); `undefined` is `0` and negatives are legal and paint below silent siblings; and it is **per sibling group only** — it creates no stacking context that escapes the parent, so a child cannot paint above its parent's sibling. That last rule is the one that decides what you write: to lift a dragged chip over a drop-zone row, put the `zIndex` on the chip's ROW, exactly as on iOS and Android. Animated (`Animated.View`, `useAnimatedStyle`) on the same terms as `opacity` — one widget write, no Yoga pass. **One divergence**: `contains()` is GTK's only per-point hook and it is consulted after a widget's children, so an _interactive native leaf_ inside a covered sibling — a `TextInput`, a `Switch`, a `ScrollView` viewport, a raw GTK widget in a slot — still takes the press even where a raised view covers it. `Text` and `Image` do not (they have no press prop here, so while something is raised they are excluded from picking and the press reaches their nearest `View`), which is also why a `pointerEvents: \"box-none\"` View whose only child is a `Text` falls through to what is behind it while a sibling in that container is raised. Measurements, the probe, the mutation check and the real-pointer proof: [research/z-index.md](research/z-index.md).\n\n10. **Pre-commit hooks regenerate derived data**: editing this file (or the other generator inputs) and forgetting to run `scripts/generate-mcp-data.mjs` no longer fails CI — the pre-commit hook regenerates `packages/react-native-gtkx/src/mcp/data/generated.ts` and stages it for you.",
768
+ doc: "docs/reference/apis.md",
769
+ heading: "PlatformColor",
770
+ text: "Supported: Adwaita theme variables — `PlatformColor(\"accent-bg-color\")`\nresolves to `var(--accent-bg-color)`; `@name` reaches a legacy named GTK\ncolor.\n\nDiffers from react-native:\n\n- The names are Adwaita's own, not iOS's or Android's.",
777
771
  },
778
772
  {
779
- doc: "docs/api.md",
780
- heading: "Globals",
781
- text: "React Native installs a whole global environment at startup —\n`Libraries/Core/setUp*.js`, run once from `InitializeCore` before any app\ncode — not just the API modules above. This platform's runtime is Node, so\nmost of that is already there natively; the rest is installed by\n`packages/react-native-gtkx/src/globals/index.ts`, called once from the top\nof the package's own entry point (`src/index.ts`) — the one place both\ntoolchains share, since both alias `react-native` onto this package (see\nPackage aliases below). Verified empirically on both the vite dev path and\nthe Metro/run-linux host, not assumed from reading RN's source.\n\n**Already node-native, nothing installed** — `fetch`/`Headers`/`Request`/\n`Response`, `Blob`/`File`, `WebSocket`, `URL`/`URLSearchParams`,\n`AbortController`/`AbortSignal`, `structuredClone`, `TextEncoder`/\n`TextDecoder`, `atob`/`btoa`, `queueMicrotask`, `setImmediate`/\n`clearImmediate`, `performance` (`.now()` confirmed monotonic), `crypto`,\n`DOMException`, and `console` (Node's own already has `group`/\n`groupCollapsed`/`groupEnd`, so RN's console polyfill — which only replaces\n`console` when a native `nativeLoggingHook` exists — is a no-op on both\ntoolchains here). `FormData` is node-native too, for the spec: **it does\nNOT understand react-native's own `formData.append('photo', {uri, type,\nname})` file-entry shape** — Node coerces the object with `String(value)`\nand silently sends the literal text `\"[object Object]\"` instead of the\nfile. Filed as its own task\n(`.claude/epics/component-gaps/xhr-formdata-uri-upload.md`) alongside\n`XMLHttpRequest`, which is not node-native at all (Node has never shipped\nit) and RN installs unconditionally\n(`Libraries/Core/setUpXHR.js`). `FileReader` is the same story —\n`Libraries/Core/setUpXHR.js` installs it too, Node has no equivalent (its\n`Blob` has `.text()`/`.arrayBuffer()`/`.stream()` but nothing like\n`readAsDataURL()`) — filed separately\n(`.claude/epics/component-gaps/filereader-missing.md`), since reading an\nin-memory `Blob` is independent of the network stack. Redirect/cookie-jar\nbehavioural differences between Node's `fetch` and RN's are out of scope —\nnoted, not chased.\n\n**Installed for parity** (`src/globals/index.ts`, each guarded so an\nexisting global always wins):\n\n- **`window = globalThis`, `self = globalThis`** — the very first thing\n react-native's own setup chain does\n (`Libraries/Core/setUpGlobals.js`, ahead of everything else in\n `InitializeCore`). Node has neither. This exists on a real RN app for the\n same reason it is worth having here: an isomorphic library's\n `typeof window !== \"undefined\"` check — often meaning \"not a server/SSR\n context, safe to run browser-shaped init\" — should read the same way on\n this platform as on any other RN platform.\n- **`navigator.product = \"ReactNative\"`** — the ecosystem's standard\n environment-detection idiom\n (`Libraries/Core/setUpNavigator.js`). Node 21 already ships a minimal\n `navigator` (`.userAgent` only, e.g. `\"Node.js/24\"` — RN itself never\n sets `userAgent`, so that value is this platform's own, not RN's), so\n this mirrors RN's exact fallback (`Object.defineProperty` onto the\n existing object) rather than assuming a bare one needs creating. Judgment\n call, resolved by evidence rather than caution: every real, currently\n running third-party package in this platform's dependency graph —\n `@gorhom/bottom-sheet`, `@gorhom/portal`, `react-native-drawer-layout`,\n the real `react-native-reanimated-dnd`, `react-native-sortables`,\n `@react-navigation/native` was grepped directly (source, not memory)\n and **none of them read `navigator.product`**; they branch on\n `Platform.OS` instead (e.g. `react-native-drawer-layout`'s\n `Drawer.native.tsx`: `swipeEnabled = Platform.OS !== 'web' &&\nPlatform.OS !== 'windows' && Platform.OS !== 'macos'`, already correctly\n `true` here since `Platform.OS` is `\"linux\"`). The real upstream\n `react-native-reanimated`/`react-native-gesture-handler` — the two\n libraries this platform substitutes with its own compat surfaces,\n precisely because their real implementations cannot run here — were also\n checked (their actual npm packages, not the compat shims): their\n `PlatformChecker`/web-detection code also uses `Platform.OS`, not\n `navigator.product`. So installing it changes no observed behaviour\n today. It is set anyway because \"RN semantics are the contract\"\n (`CLAUDE.md`) and this is exactly what a real RN app's global environment\n reports — a future library reading it (the idiom exists for a reason:\n `whatwg`-style isomorphic packages use it to pick their RN code path over\n a browser assumption) should get the RN answer, not Node's bare\n `\"Node.js/NN\"` `userAgent` with no `product` at all, which resembles\n neither a browser nor RN.\n- **`requestIdleCallback`/`cancelIdleCallback`** — RN's real one\n (`Libraries/Core/setUpTimers.js`) is a TurboModule with true native idle\n scheduling; Node has none. This installs the standard web-fallback shape\n every userland \"requestidlecallback polyfill\" package uses: fires on the\n next macrotask (`setTimeout(…, options.timeout ?? 1)`), reports a fixed\n 50ms budget through `timeRemaining()`, and `didTimeout` is always\n `false`. Good enough for \"run this off the current tick, eventually\" —\n not a real scheduling primitive, and code that depends on genuine idle\n detection should not rely on it.\n- **`global.alert`**RN's own (`Libraries/Core/setUpAlert.js`) forwards a\n single string to `Alert.alert('Alert', text)`; this does the same against\n the platform's real `Alert` (Adw.AlertDialog) module above.\n- **`ErrorUtils`** — not in RN's `InitializeCore` JS chain at all, but\n required by it: `Libraries/Core/setUpErrorHandling.js` reads\n `global.ErrorUtils` and expects it to already exist. On a real RN app it\n does, via `@react-native/js-polyfills`' `error-guard.js`, which\n `@react-native/metro-config`'s `getDefaultConfig` prepends to **every**\n Metro bundle unconditionally — independent of, and not disabled by, this\n platform's own `serializer.getModulesRunBeforeMainModule: () => []`.\n Confirmed empirically: the Metro/run-linux host already had `ErrorUtils`\n before this change (Metro's own polyfill), the vite dev path did not —\n a genuine cross-toolchain gap this closes, with a faithful port of the\n real polyfill (`setGlobalHandler`/`reportError`/`reportFatalError`/\n `applyWithGuard`/`guard`; the default handler rethrows, exactly RN's\n un-hooked behaviour).\n\n**`__DEV__`** is bundler-provided, not something either RN or this globals\nmodule sets, and both presets were checked directly rather than assumed:\nthe vite preset (`src/vite/index.ts`) defines it from vite's own `mode`\n(`__DEV__: JSON.stringify(env.mode !== \"production\")`) — found necessary\nafter a library reading it at module scope (`@gorhom/bottom-sheet`'s logger\nand four of its components) crashed the bundle with `ReferenceError:\n__DEV__ is not defined` on the vite path, where nothing else supplies it.\nThe Metro path gets it from the app's own stock `@react-native/metro-config`\npreset (`metro-transform-plugins`' inline-requires transform, driven by\n`--dev`/`NODE_ENV`), unrelated to this platform's `withLinuxPlatform` wrap.\n\n**`requestAnimationFrame`/`cancelAnimationFrame`** are a confirmed\nRN-parity gap too (real native RN provides both on every platform) but are\ndeliberately **not** installed here — a parallel, focused effort owns them\n(off `components/frame-scheduler.ts`'s GTK frame clock, with real\nnext-frame/cancel semantics this module has no reason to duplicate or\nrace). Both belong in `src/globals/index.ts` next to the installers above\nonce that work lands.\n\n**Not applicable, by architecture**: RN's Fabric-era DOM-compatibility\nglobals (`Node`, `Element`, `HTMLElement`, `Document`, `Event`,\n`EventTarget`, `CustomEvent`, `DOMRect(ReadOnly/List)`, `HTMLCollection`,\n`NodeList` — `react-native`'s own `src/private/setup/setUpDOM.js`) exist to\nback Fabric's public-instance DOM-traversal API on top of its C++ shadow\ntree. This platform has neither Fabric nor a shadow tree — its own React\nreconciler drives GTK widgets and Yoga directly — so there is no shadow\ntree to expose through a DOM-shaped facade, and installing these globals\nwithout one behind them would be a facade over nothing.",
773
+ doc: "docs/reference/apis.md",
774
+ heading: "AppRegistry",
775
+ text: 'Supported: `registerComponent`, `runApplication(appKey, { title, width,\nheight, initialProps, chrome, actionAccels, breakpoints })`, `getAppKeys`.\n\nDiffers from react-native:\n\n- These are desktop window parameters, not mobile ones.\n- `chrome: "content"` uses an `AdwApplicationWindow` with no window titlebar\n when the app declares `"Adw-1"` in its `gtkx.config.ts` — the app\'s own\n header bars become the window chrome — and falls back to the plain\n `GtkApplicationWindow` that `chrome: "system"` always uses when it does\n not. Requesting `chrome: "content"` unconditionally is the portable\n choice: header-bar chrome where Adwaita is available, an ordinary window\n otherwise, with no branch of the app\'s own.\n- `actionAccels` binds accelerators to `GtkApplication` action names;\n `breakpoints` reaches `AdwApplicationWindow` directly and only takes\n effect under `chrome: "content"` with `"Adw-1"` declaredotherwise a\n development warning names the mismatch once per run.\n- `applicationActions`/`windowActions`/`windowControllers` are superseded\n by the declarative `<ApplicationActions>`/`<WindowActions>`/\n `<WindowControllers>` components documented in [Window, navigation, and\n settings](../architecture/integration.md#actions-and-shortcuts-declared-in-the-tree); they\n still work unchanged.',
782
776
  },
783
777
  {
784
- doc: "docs/api.md",
785
- heading: "Package aliases",
786
- text: "Both presets `withLinuxPlatform` (Metro) and `reactNativeGtkx` (vite) —\nrewrite six package names during resolution, from one table\n(`packages/react-native-gtkx/src/aliases/index.ts`) that both of them read. A\nname is matched **exactly or with a `/` after it**, and the tail is\ntransplanted onto the target: `react-native-svg/lib/x` becomes\n`react-native-gtkx/svg/lib/x`, while `react-native-svg-icons` is left alone.\n\n| Package | Resolves to | Why |\n| ------------------------------------------------------------------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `react-native` | `react-native-gtkx` | **The platform.** Not a substitution and not configurable — the out-of-tree `npmPackageName` declaration alone does not alias imports for a bundle. |\n| [`react-native-svg`](#react-native-svg-compatibility-react-native-gtkxsvg) | `react-native-gtkx/svg` | The real package is a native module. |\n| [`react-native-reanimated`](#react-native-reanimated-react-native-gtkxreanimated) | `react-native-gtkx/reanimated` | The real package needs a worklet runtime and a Babel plugin. |\n| [`react-native-worklets`](#react-native-worklets-react-native-gtkxworklets) | `react-native-gtkx/worklets` | Where Reanimated 4 moved that runtime. Libraries pull `scheduleOnRN`/`scheduleOnUI` out of it at module scope, so an unaliased name fails at import. |\n| [`react-native-gesture-handler`](#react-native-gesture-handler-react-native-gtkxgesture-handler) | `react-native-gtkx/gesture-handler` | A shim, not a port: it implements `GestureHandlerRootView` and makes every other export throw where it is used. |\n| [`react-native-reanimated-dnd`](#drag-and-drop-react-native-gtkxdnd) | `react-native-gtkx/dnd` | A mirror of its API on GTK drag-and-drop. **The one that is a real choice** — see below. |",
778
+ doc: "docs/reference/apis.md",
779
+ heading: "Platform",
780
+ text: 'Supported: `OS: "linux"`, `Version` (the GTK version), `select`\n(`linux` `native` `default`), `isTV`, `isTesting`.\n\nDiffers from react-native:\n\n- `Platform.OS` is typed as the full `PlatformOSType` union plus `"linux"`,\n not a `"linux"` literal comparing it against another platform\'s name is\n a runtime question, and RN\'s own types let that compile everywhere.',
787
781
  },
788
782
  {
789
- doc: "docs/api.md",
790
- heading: "The one that is a real choice",
791
- text: "Five of the six substitute an implementation that cannot run here **at all**.\n`react-native-reanimated-dnd` stopped being one of those: the real 2.0.0 runs\non top of this platform's Reanimated, worklets and gesture-handler surfaces,\ndragged by a real pointer (the gallery's Upstream drop zones and Upstream\nsortables sections). So\nthere is a genuine trade:\n\n- **`react-native-gtkx/dnd` (default)** — GDK carries a\n `Gtk.WidgetPaintable` of the dragged view above every window, with the\n theme's own drag cursors, hit testing against the real widget tree and\n drops into _other applications_. The dragged view itself never moves.\n- **the real `react-native-reanimated-dnd`** — `dragAxis`, `dragBoundsRef`,\n `dropAlignment`, `collisionAlgorithm` and the rest of upstream's prop\n surface, and the view moves under the pointer. No drag icon, no\n cross-application drop, and the drag is confined to the app window.\n\nEverything else in [Differences from\n`react-native-reanimated-dnd`](#differences-from-react-native-reanimated-dnd)\napplies to the mirror; the real package has upstream's behaviour by\ndefinition.",
783
+ doc: "docs/reference/apis.md",
784
+ heading: "Dimensions",
785
+ text: 'Supported: `get("window"/"screen")`, `addEventListener("change")`.\n\nDiffers from react-native:\n\n- Reports the main window only transient windows are ignored.\n- `get("window")` is the app\'s own viewport: the window\'s content area\n under its header bar, the desktop analogue of RN\'s app window.',
792
786
  },
793
787
  {
794
- doc: "docs/api.md",
795
- heading: "Configuring the package aliases",
796
- text: 'Both presets take an `aliases` option: **deltas keyed by package name**, not a\nreplacement list. Anything you do not mention keeps its default, which is the\npoint — a list you have to re-state in full is a list that can silently lose\nan entry, and `ssr.noExternal` losing three of these six names is what put the\nreal `react-native-gesture-handler` into a Linux app.\n\n```ts\n// vite.config.ts\nimport { reactNativeGtkx } from "react-native-gtkx/vite"\n\nexport default defineConfig({\n plugins: [\n reactNativeGtkx({\n aliases: {\n // false — drop one of ours, so the real package loads\n "react-native-reanimated-dnd": false,\n // string — exact name or subpath, tail transplanted\n "my-pkg": "my-pkg/linux",\n // { pattern, replace } — for the rare case where the subpath\n // layouts differ\n "weird-pkg": { pattern: /^weird-pkg\\/lib\\/(.+)$/, replace: "impl/$1" },\n },\n }),\n ],\n})\n```\n\n```ts\n// metro.config.ts — the same object, the same semantics\nexport default withLinuxPlatform(getDefaultConfig(__dirname), {\n aliases: { "react-native-reanimated-dnd": false },\n})\n```\n\nPrefer the string form. It is anchored to the package name by construction,\nwhich is not a nicety: `react-native-reanimated-dnd` is a lookalike of\n`react-native-reanimated`, and `react-native-worklets-core` is a real,\nunrelated package (VisionCamera\'s) that looks like `react-native-worklets` —\na loose prefix rewrite sends either onto a subpath that does not exist. Reach\nfor `{ pattern, replace }` only when a package\'s subpath layout does not match\nits target\'s.\n\nBecause the rules are data rather than functions, the preset validates them\nwhen your config loads, and says what is wrong:\n\n- **an unknown key with `false`** — the aliases that exist are named, so a\n typo cannot silently do nothing;\n- **an overlapping pattern** — "your pattern also matches\n `react-native-reanimated-dnd`, which is declared separately". Two rules\n claiming one specifier would make resolution order-dependent;\n- **an unanchored pattern, or one with the `g`/`y` flag** — the first matches\n inside longer specifiers, the second carries a `lastIndex` between calls;\n- **a target that is not a module specifier** — a relative or absolute path,\n or one ending in `/`;\n- **`react-native`** — it cannot be dropped or retargeted, and the message\n says why: it is the platform, not one of the substituted packages.\n\nOn the vite path the option also drives `ssr.noExternal`, which is derived\nfrom the table rather than written out beside it. Every name in the table\nstays inside vite\'s pipeline — including the ones you turn off, deliberately:\nan un-aliased package still imports `react-native` at module scope, and that\nimport only reaches the platform alias if Node never gets the package first.',
788
+ doc: "docs/reference/apis.md",
789
+ heading: "useWindowDimensions",
790
+ text: "Supported: reactive main-window dimensions.",
797
791
  },
798
792
  {
799
- doc: "docs/api.md",
800
- heading: "Plain GTK profile",
801
- text: '`gtkx.config.ts`\'s `libraries` need not include `"Adw-1"`:\n\n```ts\nimport { defineConfig } from "@gtkx/config"\n\nexport default defineConfig({\n libraries: ["Gtk-4.0"],\n applicationId: "com.example.myapp",\n})\n```\n\nAn app declared this way never links libadwaita — no theming, no\n`Adw.StyleManager`, no Adwaita widgets — which is a real, supported profile,\nnot an unsupported edge case: `react-native-gtkx`\'s own bridge is split into\na core module with zero Adw imports (everything `View`/`Text`/`ScrollView`/\n`Modal`/`Animated`/gestures/`FlatList`/etc. the whole surface documented\nabove — needs) and a separate Adw module, loaded only when the codegen store\nthis config produces actually has Adw bindings. `spike/plain-gtk` in this\nrepo is exactly this profile, checked in as its own install and its own\n`gtkx codegen` run, and is what every claim below is proven against — a\ngenuinely Adw-less store, not a mocked one.\n\n**Three API rows above have a plain-GTK fallback**, so the same app code\nruns on both profiles without branching on which one it got:\n\n- **`AppRegistry`**\'s `chrome: "content"` falls back to the plain\n `GtkApplicationWindow` `chrome: "system"` already uses, instead of\n throwing see the `AppRegistry` row above. This is why requesting\n `chrome: "content"` unconditionally is the right default for a portable\n app: HeaderBar-as-chrome where Adw exists, an ordinary window chrome\n where it does not, with no `if (adwAvailable())` of the app\'s own to\n write. `breakpoints` degrades the same way `chrome: "content"` under the\n wrong chrome always has — accepted, ignored, one dev warning (warn-once),\n this one naming `"Adw-1"` as the reason instead of the chrome mismatch.\n- **`Alert.alert`** falls back to `Gtk.AlertDialog` (GTK ≥ 4.10) — see the\n `Alert` row above for exactly what is preserved (button order, default/\n cancel mapping, callbacks) and lost (`destructive`/`isPreferred`\n appearance; `cancelable: false` with no `cancel`-style button).\n- **`Appearance`/`useColorScheme`** fall back to the\n `org.freedesktop.appearance` desktop portal\'s `color-scheme` setting\n (live updates via its `SettingChanged` signal), then to\n `Gtk.Settings:gtk-application-prefer-dark-theme` when no portal answers —\n see the `Appearance` row above. The contract is identical either way:\n always `"light"`/`"dark"`, change events still fire, and `setColorScheme`\n is local to this process on both profiles, never a system-wide write.\n\n**Two subpaths need Adw unconditionally and refuse without it**:\n`react-native-gtkx/adw` (the Adwaita widget bindings) and\n`react-native-gtkx/navigation` (built on `AdwHeaderBar`/`Adw.NavigationView`\n— see [Navigation](#navigation-react-native-gtkxnavigation) below). Importing\neither on this profile throws by name, naming the fix:\n\n```\n[react-native-gtkx] "@gtkx/jsx/adw" requires "Adw-1" in this app\'s\ngtkx.config.ts `libraries` — see docs/api.md (the plain-GTK profile) for\nwhat needs Adw unconditionally and what falls back without it.\n```\n\n`react-native-gtkx/common`\'s `NavigationStack`/`NavigationStackPage` are the\nsame story at component granularity rather than import granularity: the\nsubpath itself imports fine (its barrel also carries Adw-free exports —\n`Widget`, `Icon`, `SlotContent`), and only actually **rendering** one of\nthese two throws, naming the component instead of the raw specifier\n(`[react-native-gtkx] NavigationStack requires "Adw-1" in this app\'s\ngtkx.config.ts \\`libraries\\` — ...`) — there is no fallback to degrade to,\nunlike the three rows above, because there is no non-Adwaita stand-in for\n`Adw.NavigationView` this platform ships.',
793
+ doc: "docs/reference/apis.md",
794
+ heading: "Appearance",
795
+ text: "Supported: `getColorScheme`, `setColorScheme`, `addChangeListener`. Backed\nby `AdwStyleManager`; on the plain-GTK profile (no `\"Adw-1\"`), it is\nsourced from the `org.freedesktop.appearance` desktop portal's\n`color-scheme` setting instead, with live updates through the portal's own\nchange signal, falling back further to\n`Gtk.Settings:gtk-application-prefer-dark-theme` when no portal answers.\n\nDiffers from react-native:\n\n- On every profile, `setColorScheme` writes to this process only it\n never writes a system-wide preference.\n- With no portal reachable and no explicit `setColorScheme` call yet, the\n reported scheme is whatever `Gtk.Settings` already defaults to (light),\n not an observed system value.",
802
796
  },
803
797
  {
804
- doc: "docs/api.md",
805
- heading: "Navigation (`react-native-gtkx/navigation`)",
806
- text: 'A [react-navigation](https://reactnavigation.org) stack navigator backed by\n`Adw.NavigationView` — native Adwaita page transitions, the HeaderBar back\nbutton and back gestures stay in sync with react-navigation state (the\nreact-native-windows / native-stack model). Requires the optional peer\n`@react-navigation/native` (v8), and requires `"Adw-1"` in this app\'s\n`gtkx.config.ts` `libraries` unconditionally — see [Plain GTK\nprofile](#plain-gtk-profile) above for what importing this subpath does\nwithout it.\n\n`@react-navigation/native@8` itself peers on `react-native: "*"` (unlike\n`@react-navigation/core@8`, which has no react-native peer at all). If your\napp has no `react-native` package anywhere in its tree — a vite+gtkx app\nwith no Metro side, exactly what `examples/gallery` demonstrates —\n`npm install` will print an unmet-peer-dependency warning for it. This is\nharmless: react-native-gtkx never imports anything from the `react-native`\npackage, so nothing actually needs it at runtime; the warning is npm being\nstrict about a peer range upstream declared loosely (`"*"` — any version\nsatisfies it, npm just wants the package present at all).\n\n```tsx\nimport { NavigationContainer } from "@react-navigation/native"\nimport { createStackNavigator } from "react-native-gtkx/navigation"\n\n// Run the app with chrome: "content" — the navigator\'s HeaderBars ARE the\n// window chrome (the default system chrome would add a second titlebar):\n// AppRegistry.runApplication(name, { ..., chrome: "content" })\n\nconst Stack = createStackNavigator()\n\nconst App = () => (\n <NavigationContainer>\n <Stack.Navigator>\n <Stack.Screen\n name="Home"\n component={HomeScreen}\n />\n <Stack.Screen\n name="Details"\n component={DetailsScreen}\n options={{ title: "Details page" }}\n />\n </Stack.Navigator>\n </NavigationContainer>\n)\n```\n\n- Screen `options`: `title` (HeaderBar title, defaults to the route name),\n `headerShown` (default true).\n- `createSidebarNavigator` — the desktop drawer equivalent on\n `Adw.NavigationSplitView`: a persistent native sidebar (`AdwActionRow`\n per screen, in a GtkListBox with Adwaita `navigation-sidebar` styling)\n selects between parallel screens (TabRouter semantics). Navigator prop\n `sidebarTitle`; screen `options`: `title`, `icon` (Adwaita symbolic icon\n name for the row\'s prefix), `color` (a CSS color for a colored-dot\n prefix instead of `icon` — the two are mutually exclusive per row,\n `color` wins if both are set), `count` (a badge suffix, hidden when 0 or\n unset). Run the app with `chrome: "content"` so the split view\'s\n HeaderBars are the window chrome (`examples/gallery` is built on it).\n Navigator prop `headerButtons` packs declarative native buttons into the\n content HeaderBar end (`{id, icon, tooltip, onPress}`, `icon` is an\n Adwaita symbolic name) — the gallery\'s color-scheme toggle uses it.\n Navigator prop `collapseWidth` (sp): below this width the split view\n collapses to the sidebar or the content pane alone, through a native\n `Adw.Breakpoint` wrapping the view in an `AdwBreakpointBin` — NOT a\n `useWindowDimensions` conditional (see docs/platform-layer.md, "Two ways\n to react to size"); the property flip happens inside GTK\'s own\n allocation pass, costing no React render for the resize itself. Unset by\n default — no `AdwBreakpointBin` is mounted at all, so existing consumers\n see no behavior change. Any route becoming active while collapsed\n reveals content (`AdwNavigationSplitView.showContent`, a plain native\n property write, not React state) — a row click OR a programmatic\n `navigate()`/`jumpTo()`; the native back button that then appears\n reverses it. Re-selecting the same, already-active row after that also\n reveals content again — GTK\'s `row-selected` does not refire for a\n re-click with no selection change, so this is driven by `row-activated`\n (fires on every click) in addition. The reverse direction — the split\n view\'s own back button, Escape or back gesture hiding content again — is\n observed too: it fires a `sidebarShown` event\n (`navigation.addListener("sidebarShown", …)`) on the currently active\n route, the same event-map protocol `createStackNavigator`\'s\n `transitionStart`/`transitionEnd` use. Nothing in react-navigation state\n changes when this fires — TabRouter has no "closed" concept, the same\n route stays focused, only the pane did — so it exists purely for an app\n that wants to react (`examples/tasks-nav`\'s `ContentScreen` resets its\n own in-screen "open task" state on it). Never fired for content being\n revealed (that direction is already an ordinary state change) or when\n `collapseWidth` is unset. Resizing back above `collapseWidth` and then\n back below it again does NOT reset `showContent` or the selection —\n confirmed empirically, not assumed — both simply persist across the\n round trip, the same size-class behavior a mobile master-detail app\n relies on; see docs/research/navigation-extensibility.md for the\n evidence.\n- **Which rung to reach for.** Three ways to put content in the sidebar,\n cheapest first — the same ladder react-navigation\'s own `tabBarIcon` →\n `drawerLabel` → `drawerContent` climbs: (1) `title`/`icon`/`color`/`count`\n above — the convenience; composes an `AdwActionRow`. (2) `sidebarRow`\n (screen option, below) — draw one row yourself; the navigator keeps the\n list and everything attached to it (selection, click → `jumpTo`, staying\n in step with navigation state, the collapsed reveal). (3) `sidebarContent`\n (navigator prop, below) — draw the whole pane, routing surface included.\n The reason rungs 2 and 3 exist at all, plainly: **`AdwActionRow` carries\n Adwaita\'s OWN row metrics, not a default this package picked** — measured\n at roughly 104px per row (with a prefix and/or count laid out) against\n ~40px for a plain title-only row — and nothing passed to\n `title`/`icon`/`color`/`count` changes that height. A screen on rung 1\n has no lever for it; wanting a different height or density means climbing\n to `sidebarRow` or `sidebarContent` instead.\n- Sidebar navigator props `minWidth` / `minHeight` (px, default 360×294 —\n GNOME\'s own adaptive floor): the narrowest size this navigator\'s UI\n supports, applied to the `AdwBreakpointBin` that `collapseWidth` mounts.\n Ignored when `collapseWidth` is unset, since no bin exists then. Adwaita\n cannot measure a breakpoint bin — what it contains changes with the\n breakpoints — so the bin reports a minimum of ZERO and warns that\n `width-request`/`height-request` must be set. Under `chrome: "content"`\n the bin is the window\'s own child, so that zero IS the window\'s floor:\n the window resizes straight past what the pane inside can draw, and\n Adwaita clips the pane instead of adapting it ("AdwNavigationSplitView\n exceeds AdwBreakpointBin width: requested 469 px, 360 px available" in\n the journal, felt as a list running off the right edge with its trailing\n controls cut away). An app whose content HeaderBar needs more than the\n default must raise it — measure the pane rather than guessing: a\n segmented control as `headerTitle` costs ~110px on its own and, unlike a\n title label, cannot ellipsize. `examples/tasks-nav` passes `480` for\n exactly that reason; the value stays below its `collapseWidth`, so the\n collapsed layout is still fully reachable.\n- Sidebar screen options `headerLeft` / `headerRight` / `headerTitle`:\n `() => ReactNode` — the content HeaderBar\'s own start/end/title, per\n screen, on top of the one navigator-wide default. This is what lets one\n screen\'s header change shape with ITS OWN selection (a filter toggle\n group for a list, a back button plus star/trash for an open item):\n call `navigation.setOptions({ headerLeft, headerRight, headerTitle })`\n from inside the screen, in an effect keyed on whatever local state\n decides its shape — no stack involved, and no new navigator API beyond\n the options themselves (`useNavigationBuilder` already re-resolves\n descriptor options on every `setOptions` call). `headerTitle` replaces\n the HeaderBar\'s title widget outright (unset, the page\'s own title\n shows automatically, as before). A screen\'s own `headerButtons`\n (`HeaderButton[]`, same shape as the navigator prop) replaces the\n navigator-level default entirely for that screen. **Caveat, found\n while testing this**: `setOptions` MERGES into the previously resolved\n options rather than replacing them — a call that omits `headerRight`\n does not clear a `headerRight` a PREVIOUS call set, it leaves it in\n place. A screen that flips between shapes must give every one of these\n four keys an explicit value (`undefined` counts as a real overwrite; an\n absent key does not) on every call, not just the ones currently in use.\n- Sidebar screen option `group`: `string` — the section this row belongs to.\n Consecutive screens sharing a `group` get one Adwaita section header above\n the first of them, attached with `GtkListBox.set_header_func` — the\n mechanism GNOME\'s own sidebars use. A header attached this way is a\n DECORATION owned by the row below it, not a row: it is outside the list\'s\n selection model and outside its focus chain, so the arrow keys and Tab walk\n straight past it and assistive technology never announces a row that cannot\n be activated. A header faked as a non-selectable `GtkListBoxRow` gets none\n of that. Grouping follows ROW ORDER, so screens in one group are declared\n together and a group name reappearing after a gap starts a second header\n rather than reordering anything; leave it unset on every screen (the\n default) and the list is flat. `examples/gallery` groups its sections into\n React Native / gtkx / Modules.\n- Sidebar screen option `sidebarRow`: `() => ReactNode` — draw the row\n yourself instead of letting `title`/`icon`/`color`/`count` compose one.\n Those four are a convenience, not the ceiling: they build an\n `AdwActionRow`, which brings Adwaita\'s own row metrics with it, so an app\n wanting a different shape, density or height had nothing to reach for.\n Return anything a `GtkListBoxRow` can hold — React Native content, GTK\n widgets, a differently-configured Adwaita row. The navigator keeps owning\n row BEHAVIOUR (selection, click → `jumpTo`, staying in step with\n navigation state, the collapsed reveal), so a custom row cannot drift out\n of sync with the router; only what is drawn changes. A screen that passes\n none of `icon`/`color`/`count` gets a compact `GtkListBoxRow` + label\n automatically — `AdwActionRow`\'s height is right when there IS a prefix\n and a count to lay out and pure cost when there is not. The next rung up\n is `sidebarContent`, below, for replacing the whole pane rather than one\n row.\n- Sidebar navigator prop `sidebarContent`:\n `(props: SidebarContentProps) => ReactNode` — replaces the ENTIRE sidebar\n pane\'s body, for a sidebar that needs sections, a search field, a footer,\n or anything a flat list of rows cannot express. The sidebar\'s children\n stop being "one row per screen": you draw what you like, and navigation\n is just the `jumpTo` you were handed. `SidebarContentProps` carries\n `routes` (key, name, resolved options, title, `focused`), `focusedIndex`\n and `jumpTo(name)` — use those rather than dispatching yourself, so\n selection cannot drift from navigation state. The pane\'s AdwHeaderBar and\n `sidebarTitle` still belong to the navigator: this is the body under it,\n not the chrome. Mounted as React Native content (a layout root filling\n the pane); a sidebar built from GTK widgets wraps its own tree in\n `WidgetContent`, the same escape hatch `contentLayout: "widget"` is for a\n screen body. Reach for `sidebarRow` (above) first if you only want a\n different ROW — it keeps the navigator\'s list and everything attached to\n it; this one hands over the whole pane, routing included. A sidebar with\n a search field above the list and a footer below it, still driven by the\n navigator\'s own routing:\n\n ```tsx\n <Sidebar.Navigator\n sidebarContent={({ routes, focusedIndex, jumpTo }) => (\n <View style={{ flex: 1 }}>\n <SearchField onSubmit={filterRoutes} />\n <ScrollView style={{ flex: 1 }}>\n {routes.map((route, index) => (\n <Pressable\n key={route.key}\n onPress={() => jumpTo(route.name)}\n >\n <Text\n style={{\n padding: 8,\n fontWeight: index === focusedIndex ? "700" : "400",\n }}\n >\n {route.title}\n </Text>\n </Pressable>\n ))}\n </ScrollView>\n <StorageUsageFooter />\n </View>\n )}\n >\n <Sidebar.Screen\n name="Inbox"\n component={InboxScreen}\n />\n <Sidebar.Screen\n name="Trash"\n component={TrashScreen}\n />\n </Sidebar.Navigator>\n ```\n\n `route.title` is already resolved (`options.title`, falling back to the\n route name) — no need to read `options.title` yourself. `jumpTo` reveals\n the content pane when collapsed, same as a native row click; the\n navigator, not this callback, decides that.\n\n- Sidebar navigator props `sidebarHeaderLeft` / `sidebarHeaderRight` /\n `sidebarHeaderTitle`: `() => ReactNode` — the SIDEBAR pane\'s own\n AdwHeaderBar start/end/title, the exact counterparts of the content\n header\'s `headerLeft`/`headerRight`/`headerTitle`. Until these existed the\n sidebar header was a hard-coded `<AdwHeaderBar />` and `sidebarTitle` (a\n plain string) was the only thing an app could set on it at all, so a\n sidebar\'s own "new item" action — where GNOME puts it, next to the pane\n title — had nowhere to go and ended up on the content header instead\n (`examples/tasks-nav` shipped with two indistinguishable `+` buttons for\n exactly this reason). `sidebarHeaderTitle` replaces the title widget the\n same way a screen\'s `headerTitle` does; unset, `sidebarTitle` renders as\n before. Content is mounted through the same `HeaderSlotContent` root the\n content header uses, so React Native content lays out as a horizontal,\n content-hugging cluster flush with natively packed buttons — do not\n hand-roll an `IntrinsicContent` here, a bare Yoga root defaults to\n `column` and pushes the window controls onto a second row. These are\n navigator PROPS rather than screen options on purpose: there is one\n sidebar pane shared by every screen, so its chrome sits at the level\n `sidebarTitle`/`sidebarContent` already do, and the `sidebar` prefix marks\n which header a name refers to. There is deliberately no\n `sidebarHeaderButtons` convenience mirroring `headerButtons` — arbitrary\n content is the primitive, and a one-button call site reads no better as a\n `{id, icon, tooltip, onPress}` record than as the `GtkButton` it already\n is; add it only if a real call site is worse without it.\n\n- Sidebar screen option `contentLayout`: `"react-native"` (default) or\n `"widget"` — what the screen\'s body IS. The default mounts it in a Yoga\n layout root that fills the pane, so `<View style={{ flex: 1 }}>` behaves\n the way it does anywhere else. `"widget"` packs the body into the page\n directly, with no layout root in between, for a screen whose body is a\n GTK widget tree (a `GtkScrolledWindow` around an `AdwClamp` around a\n `.boxed-list` `GtkListBox`, say): GTK\'s own sizing — `vexpand`, a list\'s\n natural height — then applies normally. **Under the default a widget tree\n collapses instead**, and quietly: every widget becomes a single Yoga LEAF\n measured for its own natural size, so a container renders its first child,\n drops the rest, and reports the ~1px it can shrink to, with no error\n anywhere. `examples/tasks-nav` is built this way. Mixing is per screen,\n not per subtree — a `"widget"` screen that wants React Native content\n somewhere inside it wraps that part in `SlotContent` itself.\n- Stack screen options `headerLeft` / `headerRight`: `() => ReactNode` —\n real RN content in the HeaderBar (inputs included), hosted by an\n intrinsic-size root; `headerButtons` render after `headerRight`\n (hn-app\'s header search filter is the demo).\n- Stack screen option `gestureEnabled: false` disables the native back\n button, Escape and the back gesture for that screen (the page\'s\n Adwaita `can-pop`); a programmatic `goBack` still pops. `usePreventRemove`\n works through the same mechanism — a prevented route reports\n `can-pop: false`, so no native pop can race react-navigation state; the\n route pops once the app lifts the guard (e.g. after its own\n confirmation dialog).\n- Stack screen option `animation` maps onto `Adw.NavigationView`\'s\n `animate-transitions` — GTK has exactly one transition style, not a\n choice of styles like iOS/Android, so the option collapses to a\n boolean: `"none"` turns transitions off, any other value (including\n native-stack\'s own style names, e.g. `"slide_from_bottom"`, `"fade"`)\n turns them on, with the standard Adwaita transition rather than the\n one asked for. Requesting a specific type still animates — it is not\n silently treated as `"none"` — and warns once in development.\n `animate-transitions` is a property of the whole view, not a per-page\n one, so there is no per-screen granularity to offer: the value used is\n read from whichever screen is currently on top of the visible stack,\n recomputed on every navigation. Setting it once via `screenOptions`\n (the same value for every screen) is the reliable way to use this —\n the per-screen case only matters if different screens genuinely\n disagree, and even then only the active one\'s value is observed.\n Interactive swipe-back gestures always animate regardless of this\n setting — Adwaita\'s own behavior, not overridable here.\n- The factories are typed: `createStackNavigator<ParamList>()` gives\n typed `Screen` configs and `StackScreenProps<ParamList, Route>` for\n screen components (`SidebarScreenProps` likewise).\n- The stack navigator emits `transitionStart` / `transitionEnd` on a\n screen\'s `navigation` object, matching `@react-navigation/stack` and\n `@react-navigation/native-stack` exactly: `{ data: { closing: boolean } }`,\n `closing: false` for the screen being pushed in, `closing: true` for the\n screen being popped out. A screen that stays mounted without actually\n entering or leaving (e.g. the screen underneath a push) gets neither\n event, same as upstream. Two things worth knowing before relying on\n timing:\n - **`transitionEnd` is tied to `AdwNavigationPage`\'s own `shown`/`hidden`\n signals** — contrary to an earlier version of this page, Adwaita DOES\n expose a transition-finished signal (four of them, in fact: `showing`,\n `shown`, `hiding`, `hidden`, all per-page). `transitionEnd` on the\n entering screen fires on that screen\'s `shown`; on the leaving screen\n it fires on `hidden`. `transitionDuration` (default 400 ms) is a\n fallback only, used when a page\'s own signal never arrives — a\n signal-less environment, or a page skipped entirely by a multi-hop\n pop (popping past an intermediate screen never fires anything on it,\n since it was never the one actually on screen during the transition).\n When transitions are not animated, the real signals still fire —\n immediately — so `transitionEnd` is not delayed by the fallback\n window either.\n - **Native pops do not fire these events at all today.** A user-driven\n pop (the Adwaita back button, Escape, the back gesture) is handled by\n the widget itself before this package\'s code is told about it, so\n there is nothing to hook a `transitionStart` into. Only\n programmatic navigation (`navigate`, `goBack`, `dispatch`, …) fires\n `transitionStart`/`transitionEnd`.\n- The sidebar navigator emits `sidebarShown` (`{ data: undefined }`) on a\n screen\'s `navigation` object — the collapsed-mode counterpart of a native\n pop, and the one case where a native, user-driven interaction (the split\n view\'s own back button, Escape, the back gesture) DOES get an event: the\n widget-level property that changes (`showContent`) has no\n react-navigation state behind it at all, so there is no state change for\n an app to observe any other way. Fired on the active route only when\n `showContent` goes from shown back to hidden, and only while\n `collapseWidth` is set; never fired for content being revealed (that\n already shows up as an ordinary focused-route change).\n- The rest of the react-navigation surface — `useNavigation`, `useRoute`,\n `useFocusEffect`, `useIsFocused`, `useNavigationContainerRef`,\n `CommonActions`, `StackActions`, `usePreventRemove`, `NavigationContainer`\n and everything else — comes from `@react-navigation/native` directly, not\n from this package. **Breaking change**: earlier versions re-exported a\n subset of these names from `react-native-gtkx/navigation`; the re-export\n was removed because it was never complete (anything beyond the subset\n still required importing from `@react-navigation/native`, so it was one\n more place to look rather than a convenience). This package\'s navigation\n entry point now exports exactly its own surface: `createStackNavigator`,\n `createSidebarNavigator`, and the option/prop types around them.\n- Each screen mounts its own layout root inside the page: the page\'s\n content allocation is that screen\'s viewport.\n- Differences from `@react-navigation/native-stack`: `headerRight`/custom\n header widgets are not supported yet; deep-link "url" events never fire\n on desktop (see `Linking`).',
798
+ doc: "docs/reference/apis.md",
799
+ heading: "useColorScheme",
800
+ text: "Supported: reactive theme.",
807
801
  },
808
802
  {
809
- doc: "docs/api.md",
810
- heading: "Svg",
811
- text: 'Vector graphics built from state, modeled on\n[react-native-svg](https://github.com/software-mansion/react-native-svg) (the\nde-facto standard RN mirrors) rather than invented from scratch — portable\ncode costs nothing to bring over. Drawing goes through `Gsk.Path`/\n`Gtk.Snapshot` on a single custom widget (`RnGtkxSvgNode`, `registerClass` +\nan overridden `snapshot()` vfunc — the same mechanism `RnGtkxLayout` and\n`RnGtkxViewBox` already use), not a rasterized image: for that, `Image`\nalready loads `.svg` files today (see the `Image` row above).\n\n**Not part of the main `react-native-gtkx` export surface** — unlike every\ncomponent in the table above, `Svg` and everything below are exported only\nfrom `react-native-gtkx/svg`, in the shape of the `react-native-svg` package\nitself. `react-native-svg` is a separate package on every other platform (RN\nhas no built-in `Svg`), so this project mirrors that split instead of adding\n`Svg` to the main entry, which would make code written against it fail to\ncompile anywhere else. See "`react-native-svg` compatibility" below for the\nexact import and how the alias resolves it.\n\n```tsx\nimport Svg, { Circle, G, Path, Rect } from "react-native-svg"\n\nconst Icon = () => (\n <Svg\n width={24}\n height={24}\n viewBox="0 0 24 24"\n >\n <Circle\n cx={12}\n cy={12}\n r={10}\n fill="#1c71d8"\n />\n <Path\n d="M8 12 l3 3 l5 -6"\n stroke="white"\n strokeWidth={2}\n fill="none"\n />\n </Svg>\n)\n```\n\n- **`Svg`**: `width`/`height` (or `style`) size it — a Yoga leaf like\n `Image`, sized entirely by style/flex, never by measuring the widget\n (nothing here is intrinsic-sized). `viewBox="minX minY width height"` and\n `preserveAspectRatio` (`xMin/xMid/xMax` × `YMin/YMid/YMax`, `meet`/`slice`,\n `none`; default `xMidYMid meet`) reshape the internal coordinate system\n exactly like real SVG — Yoga never sees them. Content always clips to the\n allocated bounds (no `overflow: visible` opt-out).\n- **`Path`**: `d` is handed straight to `Gsk.Path.parse()`, which understands\n SVG path syntax natively — there is no path parser of our own.\n- **`Rect`** (`x`/`y`/`width`/`height`/`rx`/`ry`), **`Circle`**\n (`cx`/`cy`/`r`), **`Ellipse`** (`cx`/`cy`/`rx`/`ry`), **`Line`**\n (`x1`/`y1`/`x2`/`y2`, stroke-only — no `fill` prop at all, not even\n ignored), **`Polygon`**/**`Polyline`** (`points`, `"x,y x,y …"` or\n space-separated, closed/open respectively): each is a small geometry\n helper away from the same `d` syntax, so every shape ends up drawn through\n that one `Gsk.Path.parse()` call.\n- Every shape accepts `fill`/`stroke` (a static CSS color — hex/`rgb()`/\n `hsl()`/named/`transparent`/`none`, or `"url(#id)"` referencing a\n gradient; default `fill="black"`, `stroke="none"`, matching SVG),\n `fillRule` (`nonzero` | `evenodd`), `fillOpacity`/`strokeOpacity`/\n `opacity`, `strokeWidth`, `strokeLinecap`/`strokeLinejoin`,\n `strokeDasharray`, `strokeDashoffset`.\n- **`G`** groups children under an `opacity` and/or a `transform` string —\n `translate()`/`scale()`/`rotate()`/`rotate(a,cx,cy)`/`matrix()`, the plain\n SVG transform-list syntax (`matrix()` maps directly onto\n `Gsk.Transform.matrix2d()`); `skewX`/`skewY` and the structured\n `transform={[{translateX:...}]}` array form `Animated.View` accepts are\n not supported here.\n- **Gradients**: `<Defs>` holds `<LinearGradient id x1 y1 x2 y2>` /\n `<RadialGradient id cx cy r>` (fractions 0–1 by default —\n `gradientUnits="objectBoundingBox"`, mapped against the shape\'s own\n `Gsk.Path.getBounds()`; `gradientUnits="userSpaceOnUse"` uses the\n coordinates as-is instead), each with `<Stop offset stopColor\nstopOpacity>` children (`offset` accepts `0.5` or `"50%"`). `Defs` must be\n a direct child of `Svg` (nested `Defs` are not scanned). No\n `gradientTransform`, no `spreadMethod` beyond the default pad behavior.\n An unresolvable `url(#id)` paints nothing for that fill/stroke rather\n than throwing.\n- **Animated**: the numeric props above (shape geometry, `opacity`,\n `strokeWidth`, `strokeDashoffset`) accept an `Animated.Value`/\n interpolation in place of a number. A tick mutates the widget\'s paint\n state directly and calls `queueDraw()` — the same bypass-React pattern\n `Animated.View` uses for `transform` (`setStoredTransform` +\n `queueAllocate`), just on its own invalidation channel since none of this\n touches Yoga. `G`\'s `transform` string and `d`/`points` are not\n Animated-aware (they are strings, not numbers).\n- Not in scope: `<Text>`/`<TSpan>` on a path, `<Mask>`, `<ClipPath>`, SVG\n filters, `<Use>`/`<Symbol>`/`<Pattern>`, and rasterizing arbitrary SVG\n strings at runtime (`SvgXml` — `Image` already covers SVG **files**). None\n of these have a real consumer yet; `Path`/`Rect`/`Circle`/`Ellipse`/\n `Line`/`Polygon`/`Polyline`/`G` cover icons, charts and indicators, the\n overwhelming majority of real usage.',
803
+ doc: "docs/reference/apis.md",
804
+ heading: "AppState",
805
+ text: "Supported: `currentState` (`active`/`background`), `addEventListener`.\n\nDiffers from react-native:\n\n- Driven by the window's own active/inactive state.",
812
806
  },
813
807
  {
814
- doc: "docs/api.md",
815
- heading: "`react-native-svg` compatibility (`react-native-gtkx/svg`)",
816
- text: '`react-native-gtkx/svg` re-exports the same set in `react-native-svg`\'s\nshape (`Svg` as both the default and a named export). The `react-native-gtkx/\nmetro` and `react-native-gtkx/vite` presets alias the bare `react-native-svg`\npackage name to it automatically, the same way they alias `react-native`\nitself so portable code that imports from `react-native-svg` runs\nunmodified:\n\n```tsx\nimport Svg, { Circle, Path } from "react-native-svg"\n```\n\nApps using neither preset can point their own bundler alias at\n`react-native-gtkx/svg` by hand. `react-native-svg` itself is never a\ndependency of this package and does not need to be installed the alias\nworks whether or not the real package is present.',
808
+ doc: "docs/reference/apis.md",
809
+ heading: "Alert",
810
+ text: "Supported: `alert(title, message, buttons, options)`, backed by\n`Adw.AlertDialog` (or `Gtk.AlertDialog`, GTK 4.10, on the plain-GTK\nprofile), including `cancel`/`destructive`/`isPreferred` button styles and\ndefault/cancel mapping.\n\nDiffers from react-native:\n\n- On the plain-GTK profile, `destructive`/`isPreferred` appearance is lost\n — `Gtk.AlertDialog` has no equivalent, so every button renders the same,\n though default/cancel mapping is preserved.\n- `cancelable: false` with no `cancel`-style button cannot be enforced\n there either, since `Gtk.AlertDialog` has no way to block Escape or a\n window-close dismissal; add a `cancel`-style button for identical\n behavior on both profiles.\n\n`alert` maps directly onto a native dialog on both the Adwaita and\nplain-GTK profiles; see [the Guide's plain-GTK page](../guide/plain-gtk.md)\nfor how a plain-GTK app is configured.",
817
811
  },
818
812
  {
819
- doc: "docs/api.md",
820
- heading: "Drag and drop (`react-native-gtkx/dnd`)",
821
- text: "A mirror of [`react-native-reanimated-dnd`](https://github.com/entropyconquers/react-native-reanimated-dnd)'s\nAPI, implemented on `GtkDragSource`/`GtkDropTarget`. Both presets alias the\nbare `react-native-reanimated-dnd` package name onto it, exactly as they do\n`react-native-svg` — so an app that already does drag-and-drop keeps its\nsource:\n\n```tsx\nimport { Draggable, Droppable, DropProvider } from \"react-native-reanimated-dnd\"\n```\n\n**Why a mirror and not the library.** Reanimated 4, `react-native-worklets`\nand `react-native-gesture-handler` are imported at module scope in twelve of\nits files, its sort algorithm lives inside a `useAnimatedReaction` worklet and\nits row layout inside a `useAnimatedStyle`, and its public types are written\nin `SharedValue<T>`. Full evidence in\n[research/drag-and-drop.md](research/drag-and-drop.md).\n\n**And it is the one alias that is a real choice.** Once those three surfaces\nexisted the real library ran on top of them, so an app can take upstream's own\nimplementation instead with\n`aliases: { \"react-native-reanimated-dnd\": false }` — what that trades away\nand what it buys is in [Package aliases](#the-one-that-is-a-real-choice).\n\n**A ported app changes nothing in its source.** `<GestureHandlerRootView>` —\nthe one non-drag-and-drop import such an app has, because upstream's quick\nstart puts it at the root — is covered too: `react-native-gesture-handler` is\naliased to [`react-native-gtkx/gesture-handler`](#react-native-gesture-handler-react-native-gtkxgesture-handler),\na shim that implements that root faithfully and makes every other RNGH export\nthrow where it is used. `examples/reanimated-dnd` is upstream's own example\napp, ported: its README lists every line the port had to change, and none of\nthem is a drag-and-drop call.",
813
+ doc: "docs/reference/apis.md",
814
+ heading: "Linking",
815
+ text: 'Supported: `openURL`, `canOpenURL` (`http`/`https`/`mailto`/`file`),\n`getInitialURL` (always `null`), `addEventListener("url")`.\n\nDiffers from react-native:\n\n- Opens through the system launcher.\n- There is no deep-link delivery on desktop yet `"url"` subscriptions\n are accepted but never fire.',
822
816
  },
823
817
  {
824
- doc: "docs/api.md",
825
- heading: "Which one to reach for",
826
- text: "There is one drag-and-drop API. Three sentences cover every case:\n\n- **Porting an app that already uses `react-native-reanimated-dnd`** — change\n nothing. Both presets alias the package name; the imports stay as they are.\n- **Writing a new app** — import from `react-native-gtkx/dnd`. Same names,\n same props, so the code also reads correctly to anyone who knows the\n library, and the file can move to a shared location later.\n- **Reordering by row id rather than by array index** — a `Droppable` around\n a `Draggable` per row, inside one `DropProvider`. `Sortable` owns an array\n and reports positions, which is the right shape when the component owns the\n order; when a store owns it, filters it and sorts it, the id-keyed pair is\n the one that fits. `examples/tasks-nav/src/components/task-row.tsx` is a\n worked example.\n\n`List`/`ListRow` used to offer a second, id-keyed reorder of their own. They\nare gone from `react-native-gtkx/common` entirely — they were the Adwaita\nlist _appearance_ written in React Native, and that is an app's business (see\n[platform-layer.md](platform-layer.md#listlistrowlistseparator-were-here-and-are-not-any-more)).\nNothing about `List` has anything to do with dragging any more.",
818
+ doc: "docs/reference/apis.md",
819
+ heading: "InteractionManager",
820
+ text: "Supported: `runAfterInteractions(task?)` (cancellable, then-able),\n`createInteractionHandle`/`clearInteractionHandle`, `addListener`.\n\nDiffers from react-native:\n\n- A navigation transition registers itself as an interaction, so work\n deferred with `runAfterInteractions` during a push or pop waits for the\n slide to finish.",
827
821
  },
828
822
  {
829
- doc: "docs/api.md",
830
- heading: "A dead zone in the REAL `Sortable`/`SortableGrid`, ported unchanged",
831
- text: "Porting an app onto the real package (unaliased, or via `DND_IMPL=real` —\nthe gallery's Upstream sortables section) reproduces a dead zone that is\nupstream's own arithmetic, not a compat-surface distortion this platform\nintroduces — checked directly against the published source and, for the\ngrid, an independent real-pointer measurement; see\n[research/dnd-collision-feel.md](research/dnd-collision-feel.md) for the\nfull reasoning. Both `useSortable` and `useGridSortable` floor the dragged\nitem's own rect onto a slot boundary from its TOP-LEFT corner: crossing a\nneighbour TOWARD index 0 takes about one pixel of travel; crossing one AWAY\nfrom it takes the neighbour's entire size in that axis — the item has to\narrive exactly on top of it. Measured (grid, `research/dnd-hover-flicker.md`\n§5) and read from source (list): the gallery's own `Sortable` (`ROW_HEIGHT\n= 56`) needs ~56px away from index 0 and ~1px toward it; its `SortableGrid`\n(74px tiles + 8px gaps) needs the full 82px away and ~1px toward.\n\n**This repo's own mirror deliberately diverges here, on purpose, both\ndirections symmetric.** `Sortable`/`SortableGrid` reorder by tracking the\ndragged item's own rect too — `fromIndex * slotSize` plus the pointer's delta\nsince the drag began, upstream's exact shape (`useEdgeAutoscroll`'s\n`GtkDropControllerMotion`, which already watches every motion event for edge\nautoscroll, reports each one to this tracking via a new `onDragMotion`\ncallback rather than a second controller) — but resolves the slot the item\nlands on by ROUNDING that position rather than upstream's own FLOOR: the\ndragged item's CENTRE against a slot's centre, not its top-left corner\nagainst the slot's origin (`order.ts`'s `resolveTrackedIndex`,\n`grid-order.ts`'s `resolveTrackedGridIndex` — `getGridCellFromCoordinates`\nitself is untouched, upstream parity, kept for its own row in this doc).\nMeasured with a real pointer (`tests/gtk/dnd/collision-thresholds.gtk.test.tsx`):\na 100px row/cell needs **~50-60px either way** — away from index 0 AND\ntoward it, centre grab AND an edge grab — not upstream's one-pixel-vs-full-item\nsplit. The origin that tracking measures against is the drag's own grab\npoint (`DragSourceControllers`'s `onGrab`, converted to the list's container\ncoordinates), never the first motion sample after a drag begins: under fast\npointer motion that sample can already be displaced past GDK's own\ndrag-start threshold, which would silently undercount every reading taken\nfrom it. Per-motion-event cost: the tracked-position update and slot\nresolution this adds is pure arithmetic, no FFI hop at all — ~0.003 µs median\n(settling from ~0.01 µs on the first JIT round), next to the drag layer's own\n~1.76 µs (two real GTK property writes per motion event,\n[research/dnd-differential.md](research/dnd-differential.md)). This was a\nchange to the mirror's OWN reorder mechanism, not to `Draggable`/`Droppable`'s\ndrop-zone hit-testing, which stays GDK's (`collisionAlgorithm` stays\naccepted-and-ignored there, below) — see\n[research/dnd-collision-feel.md](research/dnd-collision-feel.md) for the\ninvestigation this decision followed from.\n\n| Export | Notes |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `DropProvider` | Scopes a set of draggables and droppables. Renders a `View` (upstream renders a fragment) because `onDragging` needs a widget. `ref` gives `getDroppedItems()` and `requestPositionUpdate()`. |\n| `Draggable`, `Draggable.Handle`, `useDraggable` | The drag source. With a handle, the `GtkDragSource` attaches to the **handle's** widget, so the rest of the item stays pressable. |\n| `Droppable`, `useDroppable` | The drop target. `capacity` is enforced in GDK's `::accept`, so a full zone shows the no-drop cursor. |\n| `Sortable`, `SortableItem`, `SortableItem.Handle`, `useSortable`, `useSortableList`, `useHorizontalSortable`, `useHorizontalSortableList` | Drag-to-reorder, vertical (default) or horizontal (`direction=\"horizontal\"`). The component owns the order (upstream's contract); read the settled one from `onDrop`'s `allPositions`. Reorder-by-crossing does not care which axis a list scrolls along, so the horizontal hooks are the same mechanism under upstream's own separate names. |\n| `SortableGrid`, `SortableGridItem`, `SortableGridItem.Handle`, `useGridSortable`, `useGridSortableList` | The 2-D sibling: cells reorder the same way, in a real Yoga `flexWrap` grid rather than upstream's absolutely-positioned cells. No list-level `onMove`/`onDragStart`/`onDrop`/`onDragging` — same as upstream's own `SortableGridProps`, which has none either; wire them on each `SortableGridItem`. |\n| `DraggableState`, `ScrollDirection`, `SortableDirection`, `HorizontalScrollDirection`, `GridOrientation`, `GridStrategy`, `GridScrollDirection` | The enums, unchanged. |\n| `listToObject`, `objectMove`, `clamp` | The list utilities, as plain functions rather than worklets. |\n| `calculateGridPosition`, `calculateIndexFromRowColumn`, `listToGridObject`, `getGridCellFromCoordinates`, `reorderGridInsert`, `reorderGridSwap`, `calculateGridContentDimensions`, `findItemIdAtIndex` | The grid utilities, same reasoning. `getGridCellFromCoordinates` floors onto the cell whose top-left corner is at or before the point — upstream's own behaviour, not a bug this port fixes. |\n| `SharedValueLike<T>` | What `SharedValue<T>` degrades to: `{ value: T }` without the worklet crossing. Reads and writes work; they just do not animate. |",
823
+ doc: "docs/reference/apis.md",
824
+ heading: "DevSettings",
825
+ text: "Supported: `addMenuItem(title, handler)` (entries in the Dev Menu —\nCtrl+Shift+D in `run-linux --dev`), `reload(reason?)`.\n\nDiffers from react-native:\n\n- Silent no-ops in release builds, as in RN.",
832
826
  },
833
827
  {
834
- doc: "docs/api.md",
835
- heading: "Differences from `react-native-reanimated-dnd`",
836
- text: "The dragged view never moves — GDK carries a `Gtk.WidgetPaintable` of it\nabove every window, with the theme's own cursors and hit testing against the\nreal widget tree, including widgets React Native never created. Everything\nbelow follows from that one fact.\n\n**A dragged `Draggable`/`SortableItem` escapes any `overflow: hidden`\nancestor automatically — not a prop, the same way GDK's own drag icon is not\none.** GDK's icon already escapes any clip in this process's own tree (it is\na compositor surface, not a descendant of anything here), but that is all it\nis — a cue at the cursor this process cannot introspect. While a drag is in\nflight, a second, non-interactive `Gtk.Picture` showing a live\n`Gtk.WidgetPaintable` of the dragged row is added to a `Gtk.Overlay` wrapped\nonce around each window's real content, escaping every ancestor's clip the\nsame way any `Overlay` child does. The original dims to reduced opacity for\nthe drag's duration (restored to whatever it was, not hardcoded) rather than\ndisappearing — `react-native-draggable-flatlist`'s `activeOpacity` and\nsimilar libraries do the same. Because a `Gtk.WidgetPaintable` is a LIVE view\nof the widget it observes, GDK's own icon and this copy dim along with the\noriginal; the three are one underlying render. The copy takes no input\n(`can-target: false`) and neither hit-testing nor the responder path changes\n— both still resolve against the original widget, unchanged. Zero React\nrenders happen per frame: positioning is two widget property writes\n(`setMarginStart`/`setMarginTop`) per motion event, ~1.76 µs median, measured\nand reasoned about next to `zIndex`'s own per-call cost in\n[research/dnd-differential.md](research/dnd-differential.md#a-window-level-drag-layer-the-escape-zindex-cannot-reach).\nReparenting the dragged widget itself into the overlay was tried first and\nrefused — a 100×100 card came out 800×600 under the new parent's own size\nnegotiation, and an unmount mid-flight stranded the widget outside the tree\nReact still owned; the same document has the detail.\n\n| Prop | Behaviour here |\n| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `preDragDelay` | Accepted, ignored. GDK's `gtk-dnd-drag-threshold` already separates a tap from a drag. |\n| `collisionAlgorithm` | Accepted, ignored. GDK hit-tests the pointer; `\"center\"` is the closest of the three. |\n| `requestPositionUpdate()` | No-op. Nothing caches a slot rectangle, because GDK re-hit-tests every motion. |\n| `onLayoutUpdateComplete` | Accepted, ignored — there is no layout pass to complete. |\n| `itemHeight`, `estimatedItemHeight`, `enableDynamicHeights`, `useFlatList`, `containerHeight` | Accepted, ignored. Yoga lays rows out at their natural height, and there is no autoscroll for `containerHeight` to feed. |\n| `dragAxis`, `dragBoundsRef`, `animationFunction` | **Unsupported.** All three describe where the dragged view goes, and it never went anywhere. Kept in the type so a file shared with iOS and Android still compiles. |\n| `dropAlignment`, `dropOffset` | **Unsupported**, same reason. |\n| `positions`, `lowerBound`, `autoScrollDirection`, `itemHeights` | Real `{ value }` boxes (`SharedValueLike`), not `SharedValue`. Forwarding them with `{...rest}` works, reads work, writes do not animate. |\n| `SortableGrid`, `SortableGridItem`, `useGridSortable*`, `useHorizontalSortable*`, `SortableDirection.Horizontal` | **Not implemented.** Importing them fails at build time; passing `Horizontal` throws. |\n| Autoscroll near a container edge during a drag | Not implemented. |\n| Sortable list height | Rows are in flow layout, so the list is as tall as its rows — not `itemsCount × itemHeight`. |\n\n| Prop | Behaviour here |\n| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `preDragDelay` | Accepted, ignored. GDK's `gtk-dnd-drag-threshold` already separates a tap from a drag. |\n| `collisionAlgorithm` | Accepted, ignored. GDK hit-tests the pointer; `\"center\"` is the closest of the three. |\n| `requestPositionUpdate()` | No-op. Nothing caches a slot rectangle, because GDK re-hit-tests every motion. |\n| `onLayoutUpdateComplete` | Accepted, ignored — there is no layout pass to complete. |\n| `itemHeight`, `estimatedItemHeight`, `enableDynamicHeights`, `useFlatList`, `containerHeight`, `containerWidth` | Accepted, ignored. Yoga lays rows out at their natural height, and the mirror's own `ScrollView` measures its own viewport for autoscroll rather than trusting a hint. |\n| `dragAxis`, `dragBoundsRef`, `animationFunction` | **Unsupported.** All three describe where the dragged view goes, and it never went anywhere. Kept in the type so a file shared with iOS and Android still compiles. |\n| `dropAlignment`, `dropOffset` | **Unsupported**, same reason. |\n| `positions`, `lowerBound`/`leftBound`, `autoScrollDirection`/`autoScrollHorizontalDirection`, `itemHeights` | Real `{ value }` boxes (`SharedValueLike`), not `SharedValue`. Forwarding them with `{...rest}` works, reads work; `autoScrollDirection`/`autoScrollHorizontalDirection` are now genuinely written by the autoscroll below, the rest do not animate. |\n| `SortableDirection.Horizontal`, `useHorizontalSortable`, `useHorizontalSortableList` | Implemented. Reorder-by-crossing does not care which axis a list scrolls along — the tracked position (see the dead-zone section above) reads whichever coordinate the axis cares about — so this is `Sortable`/`useSortable`'s own machinery with a horizontal `ScrollView` and `leftBound`/`autoScrollHorizontalDirection` plumbing, not a second implementation. `gap`/`paddingHorizontal` are real Yoga layout on the content container, not hints. |\n| `SortableGrid`, `SortableGridItem`, `useGridSortable`, `useGridSortableList` | Implemented. The grid is a real Yoga `flexWrap` layout — fixed-size cells, a fixed cross-axis dimension (`columns`/`rows` × `itemWidth`/`itemHeight`) — rather than upstream's absolutely-positioned cells at a `useAnimatedStyle`-computed `top`/`left`; the same row/column arithmetic (`calculateGridPosition`) places them, a different engine paints it. `getGridCellFromCoordinates` floors onto the cell whose top-left corner is at or before a point, exactly matching upstream. `SortableGridItem`'s `isBeingRemoved` removal animation is accepted and ignored, same reason as `animationFunction` above. `scrollEnabled` is accepted and ignored too — this platform's `ScrollView` has no prop to disable input the way upstream's does. |\n| Autoscroll near a container edge during a drag | Implemented for `Sortable` and `SortableGrid`: a `GtkDropControllerMotion` on the list's own viewport reports how close the drag sits to an edge, and a `Gtk.Widget` tick callback nudges the real `GtkAdjustment` toward it for as long as it stays there — an imperative per-frame write, no React render either way. One difference from upstream: the scroll runs at a constant speed while the edge band is occupied, rather than easing into a 1500ms glide, because there is no timing engine here to ease with. Not wired into the standalone `useSortableList`/`useHorizontalSortableList`/`useGridSortableList` hooks, which build no `ScrollView` of their own to drive. |\n| Sortable list height | Rows are in flow layout, so the list is as tall as its rows — not `itemsCount × itemHeight`. |",
828
+ doc: "docs/reference/apis.md",
829
+ heading: "I18nManager",
830
+ text: "Supported: `isRTL` (a live read of the locale's text direction),\n`doLeftAndRightSwapInRTL`, `getConstants`.\n\nDiffers from react-native:\n\n- `allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops —\n mobile's persisted RTL override has no desktop store to persist to.",
837
831
  },
838
832
  {
839
- doc: "docs/api.md",
840
- heading: "`react-native-gesture-handler` (`react-native-gtkx/gesture-handler`)",
841
- text: "**Not a port of RNGH.** The semantics are reimplemented over this platform's\nown responder system, the same way `react-native-gtkx/reanimated` and\n`react-native-gtkx/dnd` are — upstream's implementation is the blueprint, not\na dependency. Two of the four reasons\n[research/gestures.md](research/gestures.md) originally gave for refusing RNGH\nexpired when Reanimated shipped; the other two (no `exports` map on its\n`src/web/`, and a react-native-windows precedent that has been a literal\n`// NO-OP` since 2.8.0) stand, which is why nothing is vendored.\n[research/gesture-detector.md](research/gesture-detector.md) has the\nmeasurements the design rests on.\n\nBoth presets alias the package name onto this subpath, so a ported app changes\nnothing in its source.\n\n```tsx\nimport { Gesture, GestureDetector } from \"react-native-gesture-handler\"\n\nconst offset = useSharedValue(0)\nconst start = useSharedValue(0)\n\nconst pan = Gesture.Pan()\n .activeOffsetY([-10, 10])\n // Capture where the view already is. `translationY` is measured from where\n // THIS gesture activated, so it starts at zero on every new grab — writing\n // `offset.value = event.translationY` instead would throw away everything\n // the view had accumulated and snap it back toward its origin the second\n // time you grab it.\n .onStart(() => {\n start.value = offset.value\n })\n .onUpdate((event) => {\n offset.value = start.value + event.translationY\n })\n\n;<GestureDetector gesture={pan}>\n <Animated.View style={[styles.card, animatedStyle]} />\n</GestureDetector>\n```\n\n`Tap` and `LongPress` are the same state machine with different predicates —\none recognizer, one event stream, one grant channel:\n\n```tsx\nconst doubleTap = Gesture.Tap()\n .numberOfTaps(2)\n // The tap-vs-drag rule: a press that travels further than this is a drag,\n // and stops being a tap.\n .maxDistance(10)\n .onStart(() => setZoomed((on) => !on))\n\nconst hold = Gesture.LongPress()\n .minDuration(400)\n // Fires with the pointer standing still — a long press activates on its\n // timer, not on the next movement.\n .onStart((event) => openMenuAfter(event.duration))\n```\n\n| Export | Behaviour |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `GestureHandlerRootView` | **Implemented, faithfully.** A `View` with `style ?? { flex: 1 }` — note that an explicit `style` _replaces_ the default rather than merging with it, which is what upstream does in all three of its implementations. Its other job, marking the subtree as gesture-arbitrating, is already this platform's: the responder system's lock is global, so there is nothing to scope. |\n| `GestureDetector` | **Implemented, and it adds no widget.** It renders its single child unchanged and reaches that child's widget through the handle the child already exposes, the same seam `createAnimatedComponent` uses. Its recognizer's responder props are merged into the child's, so a child with its own `onTouchStart` keeps working. `userSelect`, `touchAction` and `enableContextMenu` are Web-only upstream and are accepted and ignored. |\n| `Gesture.Pan()`, `Gesture.Tap()`, `Gesture.LongPress()`, `Gesture.Native()` | **Implemented**, all four over one state machine — the same event stream, the same grant channel, different predicates. See the config tables below. |\n| `Gesture.Pinch()`, `Gesture.Rotation()` | **Implemented, and they need a TOUCHPAD.** The same state machine, fed by `GtkGestureZoom`/`GtkGestureRotate` instead of by the pointer — see [the touchpad gestures](#gesturepinch-and-gesturerotation--the-two-that-need-a-touchpad) below. A mouse cannot produce either: with no touchpad attached they simply never begin. |\n| `usePanGesture()`, `useTapGesture()`, `useLongPressGesture()`, `useNativeGesture()`, `usePinchGesture()`, `useRotationGesture()`, `useFlingGesture()`, `useManualGesture()`, `useHoverGesture()` | **Implemented**, over the same recognizers. Nine hooks and ten recognizers, which is upstream's own count rather than a gap: `src/v3/hooks/gestures/` has nine directories and no `forceTouch`, `SingleGesture` omits ForceTouch from its union, and `useForceTouchGesture` exists nowhere in 3.1.0 — so `Gesture.ForceTouch()` is the whole API upstream offers for it. Upstream deprecated all twelve `Gesture.*` statics in 3.1.0 in favour of hooks, and its hook renamed the callbacks: `onStart` → `onActivate`, `onEnd` → `onDeactivate`, `onTouchesCancelled` → `onTouchesCancel`, no `onChange`, and `canceled` on the ending event instead of a second `success` argument. Both spellings are honoured as written. |\n| `Gesture.Race()`, `Gesture.Simultaneous()`, `Gesture.Exclusive()` | **Implemented as list-builders** over the three relation maps, with no mechanism of their own — see [the relations](#cross-gesture-relations) below. One `GestureDetector` may hold a composition, which mounts several recognizers on the one child and still adds no widget. |\n| `useCompetingGestures()`, `useSimultaneousGestures()`, `useExclusiveGestures()` | **Implemented**, the hook spelling of the same three, over the same lists. `useCompetingGestures` is `Gesture.Race()` under upstream's better name. |\n| `Gesture.Fling()`, `Gesture.Manual()`, `Gesture.Hover()`, `Gesture.ForceTouch()` | **Implemented**, and each was refused until now for a different reason — see [the last four](#the-last-four-fling-manual-hover-and-force-touch). `Fling` and `Manual` were reachable and unwritten. `Hover` was refused on a judgement about the test rig that was simply wrong, and is now the most fully verified of the four. `ForceTouch` needs a pressure-reporting **stylus**: it is driven by `GtkGestureStylus`, which is stylus-only, so a mouse produces no events for it at all. |\n| `GestureStateManager` | **Implemented — refused, then reversed 2026-08-05.** Upstream's standalone `activate(handlerTag)` / `fail(handlerTag)` / `deactivate(handlerTag)`, routed through the same `stateManager` object `Gesture.Manual()`'s own `onTouches*` callbacks already receive, resolved from the tag through a new registry — see [the reversal](#gesturestatemanager--the-refusal-a-real-consumer-outgrew) below. |\n| `Directions`, `HoverEffect`, `MouseButton`, `PointerType` | **Implemented**, as the plain enums they are upstream, with every value pinned by a test. `Directions` is required rather than merely harmless — `Gesture.Fling().direction()` takes those bits. `PointerType` became meaningful with `ForceTouch`, the first kind whose events are honestly not a mouse. `HoverEffect` and `MouseButton` are **inert**, exactly as they are off their platforms upstream, and are exported because the knobs that take them (`.effect()`, `.mouseButton()`) are already accepted-and-inert: a knob that takes a number while refusing the constant naming that number is incoherent. |\n| `State` | **Implemented**, as the plain enum it is upstream: `UNDETERMINED` 0, `FAILED` 1, `BEGAN` 2, `CANCELLED` 3, `ACTIVE` 4, `END` 5. Every payload carries a faithful `state`, and two of the libraries this targets compare it by value, so all six numbers are pinned by a test against 3.1.0 — a silently different one would go on compiling and quietly answer false. |\n| `ScrollView`, `FlatList`, `TextInput`, `Switch`, `Pressable` | **Implemented as the platform's own components, by identity.** Upstream builds each with `createNativeWrapper(RN.X, { disallowInterruption: true, shouldCancelWhenOutside: false })` — an RN component with a `NativeViewGestureHandler` attached, so its arbitration knows about the native scrolling underneath. Here the responder system IS that arbitration, every one of these already speaks it, and `Gesture.Native()` is how a gesture is declared over one explicitly — so the wrapper has nothing to add and the re-export is the component itself. They are here because they are RENDERED: two of the three measured consumers hand `FlatList`/`ScrollView` to `Animated.createAnimatedComponent()` at module scope. |\n| `TouchableOpacity`, `TouchableHighlight`, `TouchableWithoutFeedback` | **Implemented as the platform's own**, same reasoning. Out of scope by preference and unavoidable in fact: `@gorhom/bottom-sheet` re-exports all three from its own public entry as `BottomSheetTouchable` on every platform except iOS, so it is upstream's export rather than an app's choice. |\n| everything else | **Throws**, naming the symbol — and every remaining refusal now carries its reason rather than being a bare stub. Three groups: the RNGH **1.x component API** (`PanGestureHandler` and the eight other `*GestureHandler` components, `legacy_createNativeWrapper`), which is a second public surface over the same recognizers that upstream deprecated before it deprecated the builder, and which none of the four target libraries still uses; the **button family** (`RawButton`, `BaseButton`, `RectButton`, `BorderlessButton`, plus `TouchableNativeFeedback`, the deprecated `Touchable` mixin and `RefreshControl`), which is not RN components with a handler attached but RNGH's own native button views, with an Android ripple, `rippleColor`/`rippleRadius` and an `activeOpacity` applied by a widget this platform does not have; and the **two experimental detectors** (`VirtualGestureDetector`, `InterceptingGestureDetector`) — one drives a gesture with no view at all, the other needs an irrevocable claim this platform cannot take back. The twelve `Legacy*` aliases inherit whichever of those applies. See [what stays refused, and why](#what-stays-refused-and-why). |",
833
+ doc: "docs/reference/apis.md",
834
+ heading: "BackHandler",
835
+ text: 'Supported: `addEventListener("hardwareBackPress")`, `exitApp`.\n\nDiffers from react-native:\n\n- There is no hardware back key on desktop — subscriptions are honored,\n but nothing fires them yet.',
842
836
  },
843
837
  {
844
- doc: "docs/api.md",
845
- heading: "`Gesture.Pan()` — the config surface",
846
- text: "| Method | Behaviour |\n| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `activeOffsetX` / `activeOffsetY` / `failOffsetX` / `failOffsetY` | **Implemented.** A single number is DIRECTIONAL, by its sign — `activeOffsetX(20)` bounds the positive side only. Failure is tested before activation, and with strict comparisons where activation uses non-strict ones, so a translation exactly on a bound activates. |\n| `minDistance`, `minVelocity`, `minVelocityX`, `minVelocityY`, `minPointers`, `maxPointers` | **Implemented.** `minDistance` defaults to 10 unless an `activeOffset*` or `minVelocity*` is set, in which case those are the criteria and distance stops applying. |\n| `activateAfterLongPress` | **Implemented, and it activates on the timer** rather than on the next pointer movement — see the responder-model extension in [research/gestures.md](research/gestures.md). `0` means no hold at all, as upstream (both of its implementations guard on `> 0`). |\n| `enabled`, `shouldCancelWhenOutside`, `manualActivation` | **Implemented.** |\n| `hitSlop` | **Implemented**, in RNGH's gesture spelling rather than RN's `View` one: it can SHRINK the area (negative values), and `{ left: 0, width: 32 }` anchors a strip to one edge. |\n| the callbacks | **Implemented**: `onBegin`, `onStart`, `onUpdate`, `onChange`, `onEnd`, `onFinalize`, `onTouchesDown`, `onTouchesMove`, `onTouchesUp`, `onTouchesCancelled`. |\n| `runOnJS` | **Accepted, and does nothing** correctly. It asks for the JS runtime; there is exactly one runtime here, so every callback already runs where it is asking. |\n| `averageTouches`, `enableTrackpadTwoFingerGesture`, `cancelsTouchesInView`, `activeCursor`, `mouseButton`, `withTestId` | **Accepted, inert** each is platform-specific upstream too, and inert off its platform there. |\n| `simultaneousWithExternalGesture`, `requireExternalGestureToFail`, `blocksExternalGesture` | **Implemented** — see [cross-gesture relations](#cross-gesture-relations). |\n\nThe common configuration and the callbacks above are shared by all three\nrecognizers, minus `onUpdate` and `onChange`: upstream puts those on\n`ContinousBaseGesture`, which `Tap` and `LongPress` do not extend. A discrete\ngesture has no travel to report and the methods are not offered.",
838
+ doc: "docs/reference/apis.md",
839
+ heading: "findNodeHandle",
840
+ text: "Supported: a stable integer per mounted widget, resolvable back to it;\naccepted by `measureLayout` as its first argument, alongside a handle\nobject. Takes what RN takes: a component handle, a node handle (returned\nunchanged), `null`/`undefined`. A windowed list resolves to the\n`ScrollView` it renders, as RN's `FlatList` resolves to its own scroll\nview.\n\nDiffers from react-native:\n\n- The tag identifies the widget, not the ref: two refs onto one view\n report the same number, and a re-render that rebuilt the handle object\n does not change it.\n- It has no native manager to resolve against, so it is worth exactly what\n this platform can resolve it to`measureLayout`, and identity.\n- `null` for anything that is not a mounted host view, as in RN.",
847
841
  },
848
842
  {
849
- doc: "docs/api.md",
850
- heading: "Cross-gesture relations",
851
- text: "Three relations, three maps keyed by handler tag, and the composers are sugar\nover them. That is upstream's shape and it is reproduced because it is the\nright one — 159 lines of list-building over three primitives.\n\n| Relation | Means |\n| ------------------------------------------------------------------- | -------------------------------------- |\n| `requireExternalGestureToFail(other)` — hook: `requireToFail` | this gesture waits for `other` to fail |\n| `simultaneousWithExternalGesture(other)` — hook: `simultaneousWith` | both may be ACTIVE at once |\n| `blocksExternalGesture(other)` — hook: `block` | `other` waits for **this** one |\n\n```tsx\nconst scroll = Gesture.Pan().activeOffsetX([-10, 10]).failOffsetY([-25, 25])\n\nconst sheet = Gesture.Pan()\n .activeOffsetY([-10, 10])\n // Held in BEGAN — taking nothing, claiming nothing — until `scroll` fails.\n .requireExternalGestureToFail(scroll)\n```\n\nA relation names the other gesture with the gesture **object**, a\n`withRef()` handle to it, or a raw handler tag, exactly as upstream's\n`GestureRef` does. Memoize the gesture you point AT (`useMemo`, a ref, or a\ncontext value): both spellings rebuild their object every render, and a\nrelation written against a stale object of a gesture that has since been\nrebuilt cannot be resolved. Upstream has the same constraint and the same\nadvice.\n\n**Two locks, at two levels, and they are deliberately not merged.** The\nresponder lock keeps its one job — this interaction belongs to React Native,\none holder, one irrevocable `CLAIMED` on the source. Gesture arbitration is a\nsecond, JS-only registry that never talks to GTK, so every relation resolves\nbefore anything is claimed. The consequences are observable:\n\n- **`Simultaneous` really means two ACTIVE gestures**, each getting its own\n `onStart`/`onUpdate`/`onEnd` for the same pointer — and there is still\n exactly ONE responder while that happens, claimed once. The gesture that did\n not win the lock is driven from the touch props, which fire regardless of\n responder status; the holder reads `onResponderMove`.\n- **Mutual exclusion is the default.** Without a relation the first gesture to\n activate cancels every other gesture watching the same interaction. A\n gesture that is already ACTIVE, or parked waiting for another, is cancelled\n by nothing except an active `Gesture.Native()` upstream's rule, and the\n reason `Native` is special rather than just another recognizer.\n- **`END` and `FAILED` are not the same release.** A gesture waiting on\n another is released when that one FAILS or is CANCELLED, and **cancelled**\n when it ENDS: the thing it was deferring to actually happened, so its turn\n never comes.\n\n`Race` adds no relation at all, because racing is what happens anyway;\n`Simultaneous` is a pairwise fill of the second map; `Exclusive` is a chain\nfill of the first, where every group waits for all the groups before it. A\nnested `Exclusive` inside a `Simultaneous` stays exclusive.\n\n#### Relations across `Root`s\n\nThe responder lock is one per process, but the negotiation PATH is whatever\nGTK widget chain the interaction arrives on, and `NestedRoot`/`IntrinsicRoot`\nput native widgets both above and below RN views. The arbitration registry is\nalso process-wide and has **no tree knowledge at all** — it is keyed by\nhandler tag. What makes that safe is when a gesture enters it: **on the press,\nnot on mount.**\n\nSo:\n\n- **Two `Root`s that nest** — an island mounted inside another island's view —\n are one GTK widget chain, so both gestures are on one interaction path and\n every relation behaves exactly as it does inside a single `Root`. Native\n widgets in between take no part in the negotiation and do not break the\n chain.\n- **Two `Root`s that are disjoint** — separate windows, or sibling islands —\n can never have both gestures live in one interaction: there is one pointer\n and one session. A relation between them is expressible, resolves to a real\n handler tag, and simply never has an occasion to apply. It is not an error\n and it does not warn.\n- **`requireExternalGestureToFail` across disjoint `Root`s does not\n deadlock.** Parking only ever happens against a gesture that is live in the\n interaction under way, so a gesture in another `Root` is never waited for.\n Recording on mount instead would have made exactly this a permanent hang.\n\nThe same reasoning covers two gestures in one `Root` that the pointer cannot\nreach together: siblings never see each other's interaction, so a relation\nbetween siblings is inert for the same reason.",
843
+ doc: "docs/reference/apis.md",
844
+ heading: "Keyboard",
845
+ text: "Supported: `addListener` (honored, never fires), `removeAllListeners`,\n`dismiss`, `isVisible` (always `false`), `metrics` (always `undefined`),\n`scheduleLayoutAnimation`.\n\nDiffers from react-native:\n\n- Every event this module carries describes a _software_ keyboard\n occluding the app, and a desktop has none so none of them fire.\n- Subscriptions are real and `remove()` pairs with them, so an unmount\n never crashes on a stale listener.\n- `dismiss()` is deliberately a no-op rather than RN's own behavior: RN\n blurs the focused input as its only way to retract the keyboard, and\n doing that here would let a library's gesture steal focus from a form.",
852
846
  },
853
847
  {
854
- doc: "docs/api.md",
855
- heading: "`Gesture.Tap()` — the config surface",
856
- text: "`Tap` activates on the **release**, not on the press, which is what leaves the\ninteraction available to anything else watching the same pointer while a tap is\nstill being decided. It never holds the responder until the instant it wins.\n\n| Method | Behaviour |\n| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `numberOfTaps` | **Implemented.** Between the taps the gesture stays `BEGAN` and holds nothing; `onBegin` fires once for the whole sequence, which is upstream's shape. |\n| `maxDuration` | **Implemented**, default 500ms, re-armed on every press of a sequence. A press held past it fails on the timer, with the pointer still down. |\n| `maxDelay` | **Implemented**, default 500ms — how long the next tap may take to arrive before the sequence gives up. |\n| `maxDistance` | **Implemented.** A radius from the press, not a per-axis limit. This is the tap-vs-drag rule, and it is what lets a press that turns into a drag stop being a tap. **There is no default**, which is upstream's own behaviour: all three of its distance limits start at an \"unset\" sentinel, so an unconfigured tap accepts any travel that stays inside the view. |\n| `maxDeltaX`, `maxDeltaY` | **Implemented**, per axis, and independent of `maxDistance`. |\n| `minPointers` | **Implemented**, checked against the most pointers the interaction ever had at once. Above 1 it never activates — see the differences below. |\n| `shouldCancelWhenOutside` | **On by default**, set from the constructor exactly as upstream's `TapGesture` does. A press that wanders off the view is not a tap on it. Note that upstream's own `useTapGesture` forgets this and its builder does not; both spellings agree here. |",
848
+ doc: "docs/reference/apis.md",
849
+ heading: "LogBox",
850
+ text: "Supported: `ignoreLogs`, `ignoreAllLogs`, `install`, `uninstall` —\naccepted and ignored.\n\nDiffers from react-native:\n\n- RN's LogBox is a full-screen development overlay, and `ignoreLogs` only\n ever kept a warning out of that overlay it never filtered the console.\n There is no overlay here, so console output is already what RN's own\n console output would have been, and nothing observable is lost by\n calling it.",
857
851
  },
858
852
  {
859
- doc: "docs/api.md",
860
- heading: "`Gesture.LongPress()` — the config surface",
861
- text: "`LongPress` activates on a **timer**, with the pointer standing still — which\nworks only because of the out-of-event grant channel described in\n[research/gestures.md](research/gestures.md). Waiting for the next pointer move\nwould mean waiting forever for a press-and-hold.\n\n| Method | Behaviour |\n| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `minDuration` | **Implemented**, default 500ms. `minDuration(0)` activates on the next tick rather than synchronously inside the press, which upstream does; nothing observable here depends on the difference. |\n| `maxDistance` | **Implemented**, default 10, and measured **from the press** for the whole gesture rather than re-based at activation — upstream's `startX`/`startY` are set on pointer-down and never moved. Travelling past it before the press matures **fails** the gesture; travelling past it after **cancels** it, so `onEnd`/`onFinalize` report `false`. |\n| `numberOfPointers` | **Implemented**, and above 1 it never activates see the differences below. |\n| `shouldCancelWhenOutside` | **On by default**, as upstream sets it in both spellings. |\n| `event.duration` | **Implemented.** Milliseconds since the press, which is the point of the gesture. Upstream carries it on `LongPress` alone; here every payload has it, because there is one payload type. |\n\n**Differences from `react-native-gesture-handler`.** There is one pointer and\n`pointerType` is always `MOUSE`: the responder system fabricates one touch per\npointer, and wlroots offers no virtual-touch protocol, so `minPointers(2)`,\n`numberOfPointers(2)` and every other multi-touch configuration is unreachable\nrather than merely untested — those gestures simply never activate, which is\nthe honest outcome rather than a silently single-finger one.\n`Pinch` and `Rotation` are the exception, and they are implemented — they take\ntheir numbers from a touchpad rather than from the pointer, which is the one\ninput this platform has that carries more than one contact point. See below.",
853
+ doc: "docs/reference/apis.md",
854
+ heading: "PanResponder",
855
+ text: "![The gallery's Gestures section: a PanResponder-driven drag, and the inner/sibling responder-negotiation demo.](../shots/gallery/gestures.png)\n\nSupported: `create(config)` `panHandlers` (spread onto a `View`), the\nfull `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`,\n`numberActiveTouches`)react-native's own `PanResponder.js`, unmodified,\nrunning on this platform's own touch-history store.\n\nDiffers from react-native:\n\n- Multi-touch `gestureState` is single-touch here, since input is one\n pointer.\n- `onShouldBlockNativeResponder`'s return value is not consulted yet.\n- `onPanResponderTerminationRequest` is asked when an ancestor tries to\n take the gesture, or when an enclosing `ScrollView` scrolls; every other\n termination is GTK's own decision and arrives as an unasked\n `onPanResponderTerminate` (see [View](components/view.md)).",
862
856
  },
863
857
  {
864
- doc: "docs/api.md",
865
- heading: "`Gesture.Pinch()` and `Gesture.Rotation()` — the two that need a touchpad",
866
- text: "**Implemented, and they are the only two gestures here that a mouse cannot\nproduce.** Everything else on this page runs off the pointer. These two do not,\nbecause a pinch is not a pointer event in any sense: it is a conclusion\nlibinput draws from two fingers moving on a device it has classified as a\ntouchpad, delivered to the app as `zwp_pointer_gestures_v1` and turned by GDK\ninto a `GDK_TOUCHPAD_PINCH`.\n\nThat turns out to be a better path than upstream has. RNGH's own single-runtime\nimplementation runs a `ScaleGestureDetector` over two tracked pointers and needs\ntwo real touches; only its `Pan` has a trackpad path at all, and that one is a\nwheel-event heuristic. GTK's `gtk_gesture_zoom_filter_event` lets the touchpad\nevent straight through at two fingers and reads\n`gdk_touchpad_event_get_pinch_scale()` off it, so the scale and the angle\narrive first-class rather than being reconstructed from positions.\n\n**Recognition and arbitration are unchanged.** These are ordinary participants\nin the same registry: the same state machine, the same callbacks, the same\n`tryActivate`, the same three relation maps, the same broadcast cancel. Only the\nraw numbers come from GTK, because they are what the pointer stream physically\nlacks. There is no second arbitration path — `Gesture.Simultaneous(pinch,\nrotation)` behaves exactly as `Gesture.Simultaneous(pan, tap)` does, and a\n`Pinch` and a `Rotation` written without a relation race and cancel each other.\n\n```tsx\nconst scale = useSharedValue(1)\nconst angle = useSharedValue(0)\n\n// A photo viewer: both gestures live at once, which is what the relation buys.\nconst pinch = Gesture.Pinch().onUpdate((event) => {\n scale.value = event.scale // 1 at the start, cumulative, >1 for a spread\n})\nconst rotation = Gesture.Rotation().onUpdate((event) => {\n angle.value = event.rotation // radians since the start, positive clockwise\n})\n\n;<GestureDetector gesture={Gesture.Simultaneous(pinch, rotation)}>\n <Animated.View style={animatedStyle}>{/* ... */}</Animated.View>\n</GestureDetector>\n```\n\n| Field / method | Behaviour |\n| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `event.scale` | **Implemented, cumulative and multiplicative**, 1 at the start of the gesture — upstream's own meaning. Not re-based when the gesture activates, which is where it differs from `Pan`'s translation: upstream's `resetProgress()` resets `scale` only while the handler is not yet ACTIVE, and GTK measures from its own recognition point for the same reason. |\n| `event.scaleChange` | **Implemented** as a RATIO — upstream's `changeEventCalculator` divides for `Pinch` where it subtracts for `Rotation`, because scale composes by multiplication. On the first update it is the `scale` itself, also upstream's. |\n| `event.rotation` | **Implemented**, radians since the start of the gesture, **positive clockwise** — upstream's convention, and also libinput's and GDK's, so nothing is negated on the way through. |\n| `event.rotationChange` | **Implemented** as a difference in radians. |\n| `event.focalX` / `focalY`, `event.anchorX` / `anchorY` | **Implemented**, in the gesture VIEW's coordinates — the space upstream's `absoluteToLocal` puts them in. They come from `gtk_gesture_get_bounding_box_center()`, whose coordinates are already relative to the widget the controller is on. `absoluteX`/`absoluteY` carry the same point in window coordinates. |\n| `event.velocity` | **Implemented per SECOND** — scale-per-second for `Pinch`, radians-per-second for `Rotation`. This is a deliberate divergence; see below. |\n| activation | `Rotation` activates at **5° of accumulated rotation**, which is upstream's `ROTATION_RECOGNITION_THRESHOLD` exactly. `Pinch` activates at **5% of scale change**, which is not upstream's number and cannot be; see below. |\n| `enabled`, `hitSlop`, `manualActivation`, `runOnJS`, `withTestId`, the three relations | **Implemented**, as for every other kind. `hitSlop` is tested against the focal point. |\n| `shouldCancelWhenOutside` | **Off by default**, as upstream sets it from `PinchGestureHandler.init` / `RotationGestureHandler.init` — a pinch is not addressed to a point the way a tap is, so a focal point that drifts off the view mid-gesture does not cancel it. |\n| the callbacks | **Implemented**: `onBegin`, `onStart`, `onUpdate`, `onChange`, `onEnd`, `onFinalize`. Both gestures are CONTINUOUS upstream, so they have `onUpdate`/`onChange`. |\n| the `onTouches*` callbacks | **Accepted, and never fire.** There is no touch sequence behind a touchpad gesture to report — no pointer goes down. Upstream's do not fire on a trackpad either. |\n| pinch-specific / rotation-specific config | **There is none, upstream included.** `PinchGesture` and `RotationGesture` add zero builder methods over `ContinousBaseGesture` in 3.1.0, and v3's `PinchGestureNativeProperties` is literally `Record<string, never>`. |\n\n**Two deliberate divergences, both named.**\n\n`velocity` is **per second**, and upstream's web path computes neither of these\nthat way. `PinchGestureHandler` divides the scale delta by a millisecond\n`timeDelta` and never by 1000, so its number is a thousand times smaller than\nthe \"points per second\" its own documentation promises; Android's equivalent\nuses `timeDeltaSeconds` and agrees with the documentation.\n`RotationGestureDetector.timeDelta` is worse — it returns\n`currentTime + previousTime`, an addition rather than a subtraction, which makes\nthe denominator roughly twice a page-lifetime timestamp and the result not a\nvelocity at all. There is no single upstream number to reproduce here, so the\ndocumented unit wins. This is the same call this module already makes about a\nplain-number `hitSlop`, which upstream's web path silently ignores and its\nnative paths normalise: where web contradicts both the documentation and\nupstream's own native path, follow the documentation and say so.\n\n`Pinch` activates at **5% of scale change**, where upstream activates after two\nstages of pixels — `ScaleGestureDetector` reports nothing until the span between\nthe two touches has changed by more than 30px, and `PinchGestureHandler` then\nactivates after a further 15px from wherever that opened. Both are arithmetic\nover two touch POSITIONS, and a touchpad pinch has none: libinput hands the\ncompositor a ratio and GDK hands GTK a ratio, so there is no span in pixels\nanywhere in the chain to measure 45 of. What makes a small threshold the right\nrestatement rather than a weaker one is where upstream's sits in the pipeline:\nupstream's is the FIRST decision that a pinch is happening at all, while here\nlibinput has already made that decision — it will not emit a pinch until it has\nclassified the two fingers' motion as one rather than as a two-finger scroll.\nMeasured with a virtual touchpad, the first scale GTK reports after `begin` is\nalready about 1.09, so this gate is a second and smaller one.\n\n**How this was verified, since no test in the suite can.** A touchpad gesture\nneeds a compositor with a libinput backend, and the headless one each vitest\nworker runs against has none (`WLR_BACKENDS=headless`,\n`WLR_LIBINPUT_NO_DEVICES=1`) — measured, and it delivers nothing. So the chain\nbelow the GTK controller is measured by probe 6 in\n[research/gesture-detector.md](research/gesture-detector.md): a virtual\nmultitouch touchpad on `/dev/uinput`\n(`packages/react-native-gtkx/tests/gtk/support/virtual-touchpad.ts`, the\ntechnique libinput's own litest suite uses), real libinput classification, the\ndesktop session's real compositor, real GDK, and both a raw `GtkGestureZoom` and\nthe shipped `Gesture.Pinch()` at the far end.\n`packages/react-native-gtkx/tests/gtk/gesture-handler/touchpad-gestures.gtk.test.tsx`\ncovers the chain above it against real controllers on real widgets, and\n`tests/unit/gesture-handler/touchpad.test.ts` covers the semantics.",
858
+ doc: "docs/reference/apis.md",
859
+ heading: "Animated",
860
+ text: "![The gallery's Animated section: Animated.timing with looping, Animated.spring overshoot, and Animated.event driving a scroll-linked header.](../shots/gallery/animated.png)\n\n![The gallery's Interpolate section: a multi-stop opacity range, a mirrored extrapolate-clamp bounce, and two interpolations of one Animated.Value.](../shots/gallery/interpolate.png)\n\n`Animated` — `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`,\n`loop`, `interpolate` (numbers and `deg`/`rad` strings, with\nclamp/extend/identity extrapolation), `ValueXY` (`setValue`/`setOffset`/\n`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) and\n`event(argMapping, config?)` — reads directly off `PanResponder`'s\n`gestureState` or a `ScrollView`'s `onScroll`, mapping is positional over the\ncallback's own arguments, traversed recursively into plain objects down to a\nleaf that is a `Value`/`ValueXY`, and `config.listener` still runs after the\nmapping does. `Animated.View`'s style takes `opacity` and the whole\n`transform` array (`translateX`/`translateY`, `scale`, `scaleX`, `scaleY`,\n`rotate`/`rotateZ`) driven directly by `Animated` nodes rather than through\nReact, plus `top`/`left`/`right`/`bottom` when the node's own `position` is\n`\"absolute\"` (what makes `ValueXY.getLayout()` work), `width`/`height` where\nthe change is confined to the node that owns it, the same responder and touch\nprops `View` takes, `pointerEvents`, and `animatedProps` — because\n`Animated.View` is `createAnimatedComponent(View)`, and every `View` prop\nreaches it there.\n\nDiffers from react-native: `rotateX`/`rotateY`/`perspective` (3D transforms),\n`skewX`/`skewY` and `matrix` are not supported, and the transform origin is\nalways the component's own center — see\n[Components](components/index.md#layout-paint-and-hit-testing).\n`useNativeDriver` is accepted and ignored, with a development warning: the\ndirect path already runs at native speed, and because there is no native side\nto hand the event to, `Animated.event` always returns the plain JS handler\nregardless of `useNativeDriver`. A mapped path the real event does not carry\nis silently left unset at any depth rather than thrown — a deliberate\nwidening of RN's own traversal, which throws one level above a missing leaf.",
867
861
  },
868
862
  {
869
- doc: "docs/api.md",
870
- heading: "`Gesture.Native()` — the config surface",
871
- text: "`Native` stands for the widget UNDERNEATH the detector rather than for\nanything React Native is doing, which makes its one platform-specific rule the\nmost important thing about it: **it never takes the responder.** Taking it is\nwhat makes this platform declare `CLAIMED` on the GTK sequence and call\n`setKineticScrolling(false)` on every enclosing `GtkScrolledWindow` — RN's\n`setIsJSResponder`, which exists to stop a native scroller stealing a JS drag.\nA gesture whose whole meaning is \"the native scroller is handling this\" cannot\nbe the thing that switches the native scroller off. So it reports, and yields;\na GTK test drives a real wheel and a real drag over one and asserts the\nscroller stays live throughout.\n\n| Method | Behaviour |\n| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| activation | **Implemented**, on upstream's own rule: `BEGAN` on press, `ACTIVE` once the pointer has travelled its `DEFAULT_TOUCH_SLOP` of 15px, which is where a native scrollable would have started scrolling. A lift before that fails rather than ends. |\n| `shouldActivateOnStart` | **Implemented.** Takes the gesture on the press itself, which is upstream's shape for a native view that is a button rather than a scrollable. |\n| `disallowInterruption`, `yieldsToContinuousGestures` | **Recorded, and read by nothing yet.** Both are statements about ARBITRATION, and the registry that arbitrates is the orchestrator's. Refusing them would refuse `@gorhom/bottom-sheet`'s own configuration for knobs whose only effect is on a relation it also states explicitly. |\n| the callbacks | **Implemented**, all of them, and `Native` is CONTINUOUS upstream so it reports `onUpdate`/`onChange` travel like `Pan` does. They arrive from the touch props rather than from `onResponderMove`, because those fire regardless of responder status and this gesture never holds it. |\n| `shouldCancelWhenOutside` | **On by default**, as upstream's `NativeViewGestureHandler.init` sets it. |\n| a sequence taken away mid-drag | **Reported as a cancellation** — `onEnd`/`onFinalize` with `success: false`. See the note below; this is the one place on the platform where telling a theft from an ending needed new machinery. |",
863
+ doc: "docs/reference/apis.md",
864
+ heading: "Easing",
865
+ text: "Supported: `linear`, `ease`, `quad`, `cubic`, `in`, `out`, `inOut`,\n`bezier`.",
872
866
  },
873
867
  {
874
- doc: "docs/api.md",
875
- heading: "The last four: fling, manual, hover and force touch",
876
- text: "**All four ship, and the interesting part is that each was refused for a\ndifferent reason — only one of which turned out to be about the platform.**\nThe recon that opened this work grouped them together and its own note said two\nof them were \"reachable today and just unwritten\". Re-examined one at a time:\n\n| Recognizer | Why it was refused | What re-examining it found |\n| ---------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `Gesture.Fling()` | unwritten | Nothing was blocking it. It is a velocity predicate and a direction predicate over the machine every other kind runs on. |\n| `Gesture.Manual()` | unwritten | The smallest of the four: two constant predicates. The work was making `GestureStateManager`'s four methods real transitions rather than two transitions and two deferred flags. |\n| `Gesture.Hover()` | \"no input to run on\" | **Wrong, and inherited rather than measured.** A hover needs no button — it needs `motion_absolute` and nothing else, which is the one request the injection harness has always had. `Pressable` has shipped hover on the same GTK controller since long before this epic. It is now the most fully verified of the four. |\n| `Gesture.ForceTouch()` | needs pressure, which nothing here reports | **True of every ordinary input, and not true of the rig.** No Wayland pointer protocol carries pressure — but the tablet protocol does, and a stylus is a kernel object. See below. |\n\n#### `Gesture.Fling()` — velocity, not distance\n\nThe thing to get right, and the thing a naive test does not catch: a fling is\nnot \"the pointer travelled 200px to the right\", because a slow drag travels\nexactly as far. Upstream guards it twice and both are reproduced.\n\n| Method / rule | Behaviour |\n| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `direction` | **Implemented**, as the bitmask it is: `Directions.LEFT \\| Directions.RIGHT` accepts either. Defaults to `Directions.RIGHT`. Setting two axis bits also opens the DIAGONAL between them, with a wider cone — `UP \\| RIGHT` accepts a 45° flick that neither `UP` nor `RIGHT` accepts alone. |\n| the cones | Upstream's: 30° around each axis (±15°) and 60° around each diagonal (±30°), which tile the circle exactly. |\n| `minVelocity` | **700 units per second**, upstream's `DEFAULT_MIN_VELOCITY`, compared strictly. Not configurable upstream and not here. |\n| the deadline | **800ms** from the press, upstream's `DEFAULT_MAX_DURATION_MS`. A press that has not flung by then FAILS, whatever it is doing. |\n| `numberOfPointers` | **Implemented, and compared for EQUALITY** against the most pointers the interaction ever had — so a two-finger fling is honestly unreachable on a one-pointer platform rather than silently single-finger, the same shape `LongPress` has. |\n| when it decides | **On every move, not on the release.** A fling activates the instant it is fast enough and pointed the right way, with the button still down; the release is only the last chance. |\n| the progression | BEGAN → ACTIVE → END in one synchronous breath, with **no `onUpdate` ever** — upstream overrides `activate()` to call `end()`. `Fling` is therefore discrete, and neither spelling offers `onUpdate`. |\n\n**One documented difference from upstream, and it is the velocity itself.**\nUpstream fits a second-degree least-squares polynomial over up to 20 samples\ninside a 300ms horizon (`VelocityTracker`) and takes the linear coefficient.\nThis platform's `velocityX`/`velocityY` are the last inter-event delta — which\nis what `Pan().minVelocity()` has always used here and what every payload\nreports. `Fling` reads the same number its own event carries rather than a\nsecond, better one nothing else can see. The consequence is that this fling is\nmore sensitive to a single long frame than upstream's; the deadline and the\ncone are unaffected.\n\n#### `Gesture.Manual()` — the app owns the state machine\n\nNo configuration of its own, in either spelling, which is upstream's shape\n(`ManualGesture` adds zero builder methods; v3's `ManualGestureProperties` is\n`Record<string, never>`). It begins on the press and then decides nothing: the\n`GestureStateManager` handed to `onTouchesDown`/`onTouchesMove`/`onTouchesUp`/\n`onTouchesCancel` is the whole API.\n\n| Method | Behaviour |\n| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `.begin()` | UNDETERMINED → BEGAN. |\n| `.activate()` | BEGAN → ACTIVE, **through the ordinary arbitration**. It is a request, not a decision: it can come back parked behind `requireExternalGestureToFail`, or cancelled. Forced past `manualActivation`, as upstream's web state manager forces it. |\n| `.end()` | BEGAN or ACTIVE → END, successfully. |\n| `.fail()` | BEGAN or ACTIVE → FAILED. |\n\n**One deliberate deviation, forced by the platform.** Upstream's Manual does not\nend when the pointers lift — its documentation says so explicitly. Half of that\nis reproduced exactly: a Manual still BEGAN when the pointer comes up **stays\nBEGAN**, holding nothing. The other half is not reachable. A gesture that is\nACTIVE here is holding an _interaction_ — the responder lock, the GTK sequence,\nthe suspended scrollers — and that interaction ends when the button does.\nStaying ACTIVE past it would mean holding a lock that no longer exists,\nreceiving no further events of any kind, and never reporting an ending at all.\nSo an ACTIVE Manual ends with the interaction, successfully. `onTouchesUp` fires\nfirst and carries the state manager, so an app that wants a different ending has\nthe event to write it in.\n\n#### `GestureStateManager` — the refusal a real consumer outgrew\n\n**Refused → reversed 2026-08-05.** The original reasoning (docs/api.md's\nformer \"What stays refused\" row) was correct about upstream's OLD export:\n`GestureStateManager.create(tag)`, a factory that looks a mounted handler up\nby tag in a global `NodeManager`. This platform's manager — the one handed to\n`Gesture.Manual()`'s `onTouchesDown`/`onTouchesMove`/`onTouchesUp`/\n`onTouchesCancel` above — was always implemented; what was missing was the\nprocess-wide tag→handler registry the factory needs, and its absence was\ndeliberate: identity here is the mounted detector, and [relations](#cross-gesture-relations)\nresolve an app's own gesture object to a tag lazily, at press, for exactly the\nreason that avoids ever needing one.\n\n**The fact that changed it is `react-native-gesture-handler` 3.1.0's export\nitself, not this platform.** Read from the installed package rather than\nassumed: the value actually re-exported under this name is `./v3`'s\n`GestureStateManager` (`src/v3/gestureStateManager.ts`) — three STATIC\nmethods keyed by a handler tag, `activate`/`fail`/`deactivate` — and the old\n`.create(tag)` factory survives only as a TYPE (`LegacyGestureStateManagerType`)\nthe package no longer exports a value for. `react-native-sortables`'\n`integrations/gesture-handler/adapters/v3.ts` calls exactly the new shape:\n`GestureStateManager.activate(event.handlerTag)` from its own `onTouchesMove`,\nonce a drag survives its `dragActivationDelay` (200ms default) — the ordinary\ncase for any real drag, not an edge one. This platform's export threw on the\nproperty READ before that call could ever happen (docs/research/upstream-libraries.md,\n\"Wall 4, confirmed\").\n\n**What reversing it took.** A registry, `gesture-handler-compat/tag-registry.ts`:\nhandler tag → mounted recognizer, populated the instant `GestureDetector`'s\nruntime mints one and forgotten the instant it is disposed — a second,\nindependent map from ./relations' spec→tag one, which stays untouched and\nstill resolves lazily. `GestureStateManager.activate`/`fail`/`deactivate` look\nthe tag up and call `.activate()`/`.fail()`/`.end()` on that recognizer's own\nstate manager — the same object, same machinery, same arbitration loop\n`Gesture.Manual()`'s table above documents. A tag naming no mounted recognizer\n— never minted, or already unmounted — is not thrown on: it warns in\ndevelopment and does nothing, the same no-op ./recognizer's own state checks\nalready give an out-of-order call.\n\n| Method | Behaviour |\n| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |\n| `.activate(handlerTag)` | `stateManager.activate()` on the recognizer that tag names — BEGAN → ACTIVE, through the ordinary arbitration loop, exactly as above. |\n| `.fail(handlerTag)` | `stateManager.fail()` — BEGAN or ACTIVE → FAILED. |\n| `.deactivate(handlerTag)` | `stateManager.end()` — BEGAN or ACTIVE → END, successfully. Upstream's other name for the same transition. |\n| unknown tag | **No-op**, warned in development. Not upstream's shape (a native lookup miss), but the closest match: loud without being fatal. |\n\n#### `Gesture.Hover()` — the refusal that did not survive contact\n\nDriven by `GtkEventControllerMotion` — the same controller `Pressable` uses for\nits `hovered` state — through the same channel `Pinch` and `Rotation` arrive on.\nIt goes straight to ACTIVE on the crossing with no threshold at all (upstream's\n`begin(); activate();` on one event), reports `x`/`y` in the gesture view's own\ncoordinates while the pointer moves inside, and ENDs — not cancels — when the\npointer leaves.\n\n**It never takes the responder**, for the same structural reason the touchpad\ngestures do not: the responder lock is a lock over an interaction, an\ninteraction starts with a press, and a hover has none. There is no session to\ntake and no GTK sequence to claim. A hover therefore cannot exclude a press.\n\n| Method | Behaviour |\n| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |\n| `.effect()` / `hoverEffect` | **Accepted, inert.** iOS's own pointer effect; nothing in upstream's web handler branches on it either. |\n| `hitSlop`, `enabled` | **Implemented**, including the shrinking (negative) form of `hitSlop`. |\n| the callbacks | **Implemented**; `Hover` is continuous, so `onUpdate`/`onChange` report travel and `changeX`/`changeY` carry the delta. |\n| `mouseButton` | Inert for this kind on both platforms — upstream's hover entry points never consult a button. |\n\n**One thing to know before putting a hover next to something.** Mutual exclusion\nis upstream's default and is reproduced, and a hover activates whenever the\npointer crosses in — so a hover entering while a pan on another view is still\nBEGAN will cancel that pan. Upstream behaves the same way and works around it\nper-use: its own `Pressable` sets `manualActivation` on the hover recognizer\nprecisely to stop it blocking a `Gesture.Native()`. Declare\n`simultaneousWithExternalGesture` (or `Gesture.Simultaneous()`) between a hover\nand anything sharing its screen. Inventing an exemption here would be a second\narbitration rule upstream does not have.\n\n#### `Gesture.ForceTouch()` — pressure, and exactly how far it is verified\n\n**Upstream does not implement this off iOS at all**, so there is no web\nbehaviour to restate and the semantics below come from the documented contract:\nthere is no `src/web/handlers/ForceTouchGestureHandler.ts`, the web `Gestures`\nregistry has nine entries and this is not one of them, the legacy component\nresolves to a `ForceTouchFallback` that warns once and renders its children\nunchanged, and there is no v3 hook.\n\n| Method | Behaviour |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `minForce` | **Implemented**, defaulting to upstream's documented **0.2**. Non-strict at the bound, like every other activation threshold here. Note that upstream's 0.2 is a doc comment rather than a constant — no JavaScript in 3.1.0 assigns it, because the real default lives in iOS code. |\n| `maxForce` | **Implemented** as a ceiling that FAILS the gesture before activation and CANCELS it after — the shape `LongPress`'s `maxDistance` has. Unset means no ceiling, which is upstream's shape too. |\n| `feedbackOnActivation` | **Accepted, inert.** There is no haptic device on this platform. |\n| `force`, `forceChange` | On every payload. `forceChange` is a **difference** (upstream's calculator subtracts, where the one for `Pinch` divides); on the first update it is the force itself. |\n| `pointerType` | **`STYLUS`**, and this is the only kind that does not say `MOUSE`. A pressure reading can only have come from a tablet tool. |\n\n**Where it comes from, and what a mouse does.** `GtkGestureStylus`, whose\n`down`/`motion`/`up` signals carry `get_axis(GDK_AXIS_PRESSURE)` already\nnormalised to `[0, 1]` — which is upstream's documented range, so nothing is\nrescaled. The controller is left at GTK's default `stylus-only`, so **a mouse\nproduces no events for it whatsoever**. That is deliberate and it is what keeps\na `ForceTouch` from quietly activating at pressure 0 on a machine with no\ntablet; a GTK test injects a real mouse press and drag over one and asserts that\nnothing fires.\n\n**How far it IS verified, measured rather than asserted.** A uinput virtual pen\ntablet (`tests/gtk/support/virtual-stylus.{py,ts}`, built from libinput's own\nlitest Wacom descriptor) drives the shipped recognizer through the whole real\nchain — kernel, evdev, libinput, compositor, GDK, `GtkGestureStylus` — under\n`spike/gesture-detector/run-stylus.sh`. It reports `begin=1 start=1 updates=13\nend=1 success=true`, activation at force **0.2298** against a `minForce` of 0.2,\nmonotonically rising forces to **1.000**, `pointerType: STYLUS`, a `maxForce`\ncancellation at **0.5196** against a ceiling of 0.5 with no update after it, and\nzero callbacks on a card the pen never touched. What it cannot do is run inside\nthe vitest suite, whose compositor is started with `WLR_BACKENDS=headless\nWLR_LIBINPUT_NO_DEVICES=1` and enumerates no input devices at all — the same\nsplit `Pinch` and `Rotation` already live with, and a property of the\ncompositor rather than of the gesture.",
868
+ doc: "docs/reference/apis.md",
869
+ heading: "version",
870
+ text: "Supported: the package version.\n\nDiffers from react-native:\n\n- Extension: not part of RN's own API.",
877
871
  },
878
872
  {
879
- doc: "docs/api.md",
880
- heading: "What stays refused, and why",
881
- text: "The refusals that remain are decisions rather than gaps, and each now carries\nits reason in the source as well as here. Re-checked against the four target\nlibraries rather than assumed: sweeping the shipped sources of\n`@gorhom/bottom-sheet` 5.2.14, `react-native-draggable-flatlist` 4.0.3,\n`react-native-drawer-layout` 4.2.9 and `react-native-reanimated-dnd` 2.0.0 for\nevery symbol still refused finds exactly one hit — `RefreshControl` in\n`@gorhom/bottom-sheet` — and it is not this package's: it comes from\n`react-native`, as a type in one file and as a value only in a `.android.tsx`\nsibling, which Metro on this platform never resolves. **Nothing reaches for a\nbutton, a legacy handler component, or any `Legacy*` alias.** The `Touchable`\nsubset shipped earlier remains the only thing upstream's own exports forced.\n\n| Refused | Why |\n| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| the nine `*GestureHandler` components and `legacy_createNativeWrapper` | The RNGH **1.x component API**, deprecated upstream years before the builder was, and migrated off by all four target libraries. Reimplementing it means a second public surface over the same recognizers, with its own `onGestureEvent`/`onHandlerStateChange` shape and its own prop plumbing, for zero measured consumers. `createNativeWrapper` specifically has nothing to add: the responder system IS the arbitration it registers with, every component here already speaks it, and `Gesture.Native()` is how a gesture is declared over one explicitly. |\n| `RawButton`, `BaseButton`, `RectButton`, `BorderlessButton` | Not RN components with a handler attached — RNGH's own **native button views**, in Java and Objective-C, with an Android ripple, `rippleColor`/`rippleRadius`, `borderless` drawable selection and an `activeOpacity` applied by the native view. There is no GTK widget with those semantics and no way to fake the ripple, so any implementation would be a `Pressable` wearing another name. |\n| `TouchableNativeFeedback`, `Touchable`, `RefreshControl` | Android's ripple by another name; RN's deprecated mixin; and pull-to-refresh, which needs a scroll gesture this `ScrollView` does not expose and a spinner widget this platform does not have. |\n| `VirtualGestureDetector` | 3.1.0's new experimental detector, which drives a gesture with no view at all. `gesture-handler-compat/tag-registry.ts` answers \"which mounted recognizer does this number mean\", not \"mint a recognizer with nothing to measure\" — every recognizer here is still built by a mounted `GestureDetector` wrapping exactly one child, and giving it none is a structural feature the [`GestureStateManager` reversal](#gesturestatemanager--the-refusal-a-real-consumer-outgrew) below did not add. **Not the same refusal `GestureStateManager` had** — see that entry for why the two used to be grouped and no longer are. |\n| `InterceptingGestureDetector` | 3.1.0's new experimental detector, which intercepts events destined for views BELOW it. On this platform that would mean claiming a GTK sequence before deciding — and `CLAIMED` is irrevocable here, so \"intercept, look, maybe give it back\" is not expressible. |\n| `GestureDetectorType` | A TYPE upstream, not a value. Type positions never reach this module at all (the alias is a bundler alias; `tsc` resolves the real package's types from node_modules), so a runtime value under that name could only be read by code that has already gone wrong. |\n| the twelve `Legacy*` aliases | Each is 3.x's escape hatch back to the 2.x implementation of a component whose 3.x spelling is either implemented here already or refused above with its own reason. Where the modern name works, the alias would be a second name carrying a promise — \"this behaves like 2.x did\" — that this platform cannot keep, never having implemented 2.x to differ from. `LegacyDrawerLayoutAndroid` is refused twice over: React Native itself does not ship `DrawerLayoutAndroid` off Android, and `@react-navigation/drawer` reaches for `react-native-drawer-layout`, which runs here. |",
873
+ doc: "docs/reference/dnd.md",
874
+ heading: "Why a mirror, not the library",
875
+ text: "`react-native-reanimated-dnd` cannot run on this platform as published.\nReanimated 4, `react-native-worklets` and `react-native-gesture-handler` are\nimported at module scope in twelve of its files, its sort algorithm lives\ninside a `useAnimatedReaction` worklet, its row layout inside a\n`useAnimatedStyle`, and its public types are written in `SharedValue<T>`.\nThis subpath re-implements the same API surface as plain functions and\ncomponents over real GTK widgets instead.",
882
876
  },
883
877
  {
884
- doc: "docs/api.md",
885
- heading: "A native ancestor stealing the sequence is no longer a clean release",
886
- text: "The correction [research/gestures.md](research/gestures.md) records, now\nimplemented. GTK's claim propagation is asymmetric: a claim by a **descendant**\ncancels the ancestor's gesture (`::cancel` then `::end`), while a claim by an\n**ancestor** DENIES the descendant and then ends it with an ordinary\n`drag-end` the same signal a finger lifting produces. This platform mapped\n`drag-end` to `onResponderRelease` and `onTouchEnd`, so a native widget above\nyou stealing a drag arrived in JS as a **clean, successful ending** at whatever\nposition the theft happened at.\n\n`responder/use-responder.ts` now watches `::sequence-state-changed` for the\n`->DENIED` transition, which is the only thing that separates the two, and\nroutes a denied `drag-end` to the cancel path. So a stolen interaction reaches\n`onResponderTerminate` and `onTouchCancel`, and every recognizer built on them\nreports it as a cancellation.\n\nNothing depended on this before, and the reason is measured rather than\nassumed: a view that takes the responder on PRESS makes the platform claim on\nits own gesture, and a claim by the descendant cancels every ancestor's gesture\noutright — so an ancestor never gets a second chance to steal. It becomes\nload-bearing with `Gesture.Native()`, which is the first recognizer that\ndeliberately never claims.",
878
+ doc: "docs/reference/dnd.md",
879
+ heading: "Opting out of the mirror",
880
+ text: 'Once the gesture-handler and Reanimated-compatible surfaces exist as their\nown shims, the real `react-native-reanimated-dnd` package can run on top of\nthem unmodified. An app can choose that instead of the mirror with a bundler\nalias override (`aliases: { "react-native-reanimated-dnd": false }`), which\nfalls through to upstream\'s own implementation. See\n[reanimated-compat.md](reanimated-compat.md) for what the Reanimated surface\nunderneath it provides.',
887
881
  },
888
882
  {
889
- doc: "docs/api.md",
890
- heading: "The two libraries this surface was measured against RUN",
891
- text: "**Measured by building them and then driving them, not by reading them.** The\nprobe app is committed at `spike/core-exports` — the real published packages\nunder the real `gtkx build`, with the presets' aliases in place, and a real\n`zwlr_virtual_pointer_v1` aimed at coordinates taken from `measureInWindow`\non the running tree. `bash spike/core-exports/run-headless.sh` in the VM\nrebuilds it, drives it and prints:\n\n```\n[core-exports] PASS the window fills the output, so window coordinates are output coordinates — columns rect = 0,0 1024x708\n[core-exports] draggable onDragBegin\n[core-exports] draggable order=b,c,a,d,e\n[core-exports] PASS the dragged row changed place — row-a y 40 -> 160\n[core-exports] PASS CONTROL: a plain ScrollView scrolls under the same injected wheel — sv row-one y 40 -> -215\n[core-exports] PASS an unstyled scrollable in a bounded parent is a viewport and scrolls unstyled row-one y 406 -> 278\n[core-exports] PASS the sheet's own scrollable receives scroll events at all — sheet list onScroll calls = 162\n[core-exports] PASS COLLAPSED: the sheet holds its list at the top under a real scroll — sheet row-one y 559 -> 559\n[core-exports] PASS the sheet moved up under the drag — handle y 531 -> 212\n[core-exports] PASS EXTENDED: the lock releases and the same wheel scrolls the sheet's list — sheet row-one y 240 -> -84\n[core-exports] PASS NEGATIVE CONTROL: the zone the pointer never visited saw nothing — control touch events = 0\n[core-exports] DONE all checks passed\n```\n\nThat method is the point, and it corrected this section four times running.\nTwice the blockers were predicted from sources and both lists were wrong; the\nthird time the list was right about the BUILD and could not have known what\nthe first render would hit; the fourth time the probe overturned a diagnosis\nthis file had already written down as settled — the sheet's list was blamed on\nthe driven-size carve-out, and the animated height it names was never a number\nat all. Nothing below was reasoned about — each entry is\nan error the toolchain produced, in the order it produced it.\n\n**`react-native-draggable-flatlist` 4.0.3.** Stopped at BUILD on\n`react-native` rather than on `react-native-gesture-handler`:\n[`findNodeHandle`](#api-modules) and [`LogBox`](#api-modules)\n(`components/CellRendererComponent`, `components/NestableDraggableFlatList`),\nthen [`useAnimatedScrollHandler`](#implemented)\n(`components/DraggableFlatList`, `components/NestableScrollContainer`). Past\nthe build it hit three more walls that only a running app can show:\n\n1. `__DEV__` was not defined on the vite path at all RN's own dev flag,\n which the Metro path gets from the app's stock preset and nothing supplied\n here. The preset defines it from vite's mode now;\n2. `CellRendererComponent`, which the windowed list did not accept. That prop\n is the library's whole design (the cell is what translates, and what\n provides the \"am I the active row\" context), so `ScaleDecorator` threw\n `useIsActive must be called from within CellProvider!` on first render;\n3. `useDerivedValue(() => withSpring(…))` — a documented Reanimated pattern\n and the shape of `hooks/useOnCellActiveAnimation`. The first evaluation of\n an updater has nothing to animate FROM, so upstream collapses every\n builder to its target for that one run; this platform seeded the shared\n value with the animation OBJECT instead, and the second evaluation failed\n with \"an animation can only be assigned to a shared value holding a\n number\".\n\n**`@gorhom/bottom-sheet` 5.2.14.** Stopped at BUILD on the same surface plus\ntwo: `findNodeHandle`, `LogBox`, [`Keyboard`](#api-modules) and\n[`VirtualizedList`](#components) (`hooks/useGestureEventsHandlersDefault`,\n`hooks/useScrollableSetter`, `utilities/findNodeHandle`,\n`components/bottomSheetScrollable`). Everything it takes from THIS surface\nresolved already — `Gesture.Native()`, `State`, the `Touchable` family it\nre-exports as `BottomSheetTouchable`, `TextInput`, and the cross-gesture\nrelations its pan chains configure. Its one running wall was\n`findNodeHandle` answering `null` for a list: it identifies its scrollable by\nnode handle (`hooks/useScrollable`), and a composite that resolved to nothing\nleft the sheet warning `Couldn't find the scrollable node handle id!` with no\nscrollable bound. A windowed list resolves to the `ScrollView` it renders\nnow, which is what RN does for a `FlatList` too.\n\n**`react-native-reanimated-dnd` 2.0.0 never loads, by design; and it RUNS\nwhen it does.** Both presets alias the package name onto\n[`react-native-gtkx/dnd`](#drag-and-drop-react-native-gtkxdnd), which mirrors\nits API on GTK's own drag-and-drop, so an app never resolves the real package\nunless it asks to. The gallery's \"Upstream drop zones\" and \"Upstream\nsortables\" sections ask `aliases:\n{ \"react-native-reanimated-dnd\": false }` (see [Package\naliases](#configuring-the-package-aliases)) — and installs it for real:\n`Draggable`, `Droppable`, `DropProvider` and `Sortable` all work on this\nsurface, dragged by a real pointer. What that took, and the two things that\nstill differ, are in\n[research/upstream-libraries.md](research/upstream-libraries.md).\n\n#### What the probe does NOT prove\n\n- **Anything about touch.** The pointer is a mouse, as everything on this rig\n is; a drag that a finger would arbitrate differently against a scroller is\n the same gap `research/gestures.md` records.\n- **The rest of either library's surface.** One draggable list and one sheet\n with a scrollable in it were driven. `BottomSheetModal`, the backdrop, the\n footer, `NestableScrollContainer`, horizontal lists and `enableDynamicSizing`\n edge cases are built but not driven.\n- **That every accepted-and-ignored call is harmless in every configuration.**\n `Keyboard`'s events never firing is right on a desktop and is still a\n behaviour difference: a sheet with `keyboardBehavior` configured has nothing\n to react to, and its keyboard state stays `UNDETERMINED`.\n\nThe throws are still the point where something IS missing. A\n`PanGestureHandler` that quietly rendered its children without gestures is\nexactly the trap [research/gestures.md](research/gestures.md) records\n`Animated.View` falling into — compiled, ran, did nothing. The stand-ins fail\non call, on render and on property access, while still answering the\nintrospection React and `console.log` do first, so the message that surfaces\nis the precise one — and `createAnimatedComponent` is the honest limit of\nthat: it reads only `displayName` and `name`, both on the allowlist, so a\nstand-in it is handed binds without complaint and cannot fail until it is\nrendered. That is why the earlier claim that RNGH's `FlatList`/`ScrollView`\nre-exports stopped `draggable-flatlist` at import was wrong.\n\nA symbol this shim does not list at all fails earlier still, at bundle time,\nwith the bundler's own \"no export named X\".",
883
+ doc: "docs/reference/dnd.md",
884
+ heading: "One drag-and-drop API, two shapes",
885
+ text: "- **Porting an app that already uses `react-native-reanimated-dnd`** —\n nothing changes. Both presets alias the package name; the imports stay as\n they are.\n- **Writing a new app**import from `react-native-gtkx/dnd` directly. Same\n names, same props, so the code reads correctly to anyone who knows the\n library.\n- **Reordering by row id rather than by array index** a `Droppable` around\n a `Draggable` per row, inside one `DropProvider`, is the right shape when a\n store owns the order, filters it and sorts it. `Sortable` owns an array and\n reports positions, which fits when the component itself owns the order.\n\n`List`/`ListRow` are not part of `react-native-gtkx/common`'s export\nsurface — that would have been Adwaita's list appearance written in React\nNative, an app's own concern, with its own id-keyed reorder bundled in. An\napp that wants id-keyed reordering combines `Droppable` and `Draggable` per\nrow instead, as above. See\n[Overview](../architecture/overview.md#react-native-gtkxcommon-what-has-no-upstream-counterpart)\nfor what `react-native-gtkx/common` does and does not carry.",
892
886
  },
893
887
  {
894
- doc: "docs/api.md",
895
- heading: "`react-native-reanimated` (`react-native-gtkx/reanimated`)",
896
- text: "Reanimated's **semantics**, reimplemented on a platform that has none of its\narchitecture — because it needs none of it. Both presets alias the bare\npackage name onto this subpath, so an app keeps its source:\n\n```tsx\nimport Animated, {\n useAnimatedStyle,\n useSharedValue,\n withTiming,\n} from \"react-native-reanimated\"\n```\n\n**Why the architecture is gone.** Reanimated exists to cross a thread\nboundary. Here GTK's main loop _is_ the JS thread a widget call is a\nsynchronous C call on the same stack so a worklet is an ordinary function,\n`measure()` is synchronous, and a shared value is an observable box. That is\nnot this project's reinterpretation: upstream ships the flattened version\nitself, selects it with `SHOULD_BE_USE_WEB`, and routes react-native-windows\n(no DOM, no second runtime) down it. Full evidence, and what it costs, in\n[research/reanimated.md](research/reanimated.md).\n\n**Why a reimplementation and not the library.** ~35,700 lines of `src/`, 21\nDOM-bound files, and a `Platform.OS` gate that does not know about `linux`;\nrunning it would mean maintaining a fork of a fast-moving dependency. The web\npath is the blueprint every behaviour here was read off it and its pure\nparts (`interpolate`, `Easing`, the spring config maths) are ported.\n\n**The Babel plugin is not needed, and not assumed.** Its output is an ordinary\nlexical closure with metadata and no injected runtime import, so `'worklet'`\nis an inert directive. This platform never runs Babel (vite/rolldown; the\nMetro path uses the app's own stock preset), while an app that also targets\niOS or Android keeps the plugin for those builds so both configurations\nwork. Dependency tracking here is **dynamic**: a mapper subscribes to the\nshared values it actually reads, which is more precise than a static\n`__closure` scan (a conditional read is tracked correctly) and needs no build\nstep. `dependencies` arrays are accepted and control only when a mapper is\nrebuilt.",
888
+ doc: "docs/reference/dnd.md",
889
+ heading: "The exported surface",
890
+ text: "- **`DropProvider`** Scopes a set of draggables and droppables. Renders\n a `View` upstream renders a fragment — because `onDragging` needs a\n widget to attach to. Its `ref` gives `getDroppedItems()` and\n `requestPositionUpdate()`.\n- **`Draggable`, `DraggableHandle`, `useDraggable`** The drag source.\n With a handle, the `GtkDragSource` attaches to the handle's widget only,\n so the rest of the item stays pressable.\n- **`Droppable`, `useDroppable`**The drop target. `capacity` is\n enforced in GDK's `::accept`, so a full zone shows the no-drop cursor.\n- **`Sortable`, `SortableItem`, `useSortable`, `useSortableList`,\n `useHorizontalSortable`, `useHorizontalSortableList`** Drag-to-reorder,\n vertical by default or horizontal (`direction=\"horizontal\"`). The\n component owns the order upstream's own contract read the settled\n one from `onDrop`'s `allPositions`.\n- **`SortableGrid`, `SortableGridItem`, `useGridSortable`,\n `useGridSortableList`** The 2-D sibling: cells reorder the same way, in\n a real Yoga `flexWrap` grid rather than upstream's absolutely-positioned\n cells. There is no list-level `onMove`/`onDragStart`/`onDrop`/\n `onDragging` here, matching upstream's own `SortableGridProps` wire\n those on each `SortableGridItem` instead.\n- **`DraggableState`, `ScrollDirection`, `SortableDirection`,\n `HorizontalScrollDirection`, `GridOrientation`, `GridStrategy`,\n `GridScrollDirection`** The enums, unchanged from upstream.\n- **`clamp`, `listToObject`, `objectMove`**The list-order utilities, as\n plain functions rather than worklets.\n- **`calculateGridPosition`, `calculateIndexFromRowColumn`,\n `listToGridObject`, `getGridCellFromCoordinates`, `reorderGridInsert`,\n `reorderGridSwap`, `calculateGridContentDimensions`,\n `findItemIdAtIndex`** The grid utilities, same reasoning: plain\n functions, not worklets. `getGridCellFromCoordinates` floors onto the\n cell whose top-left corner is at or before the point, exactly matching\n upstream's own behavior.\n- **`SharedValueLike<T>`** What `SharedValue<T>` degrades to: `{ value:\nT }`, without the worklet crossing. Reads and writes work; they just do\n not animate.\n\nDeliberately not re-exported: `setPosition`, `setAutoScroll`,\n`setGridPosition`, `setGridAutoScroll`. Upstream exports these as worklet\nhelpers that mutate a `SharedValue` mid-gesture, driven by a UI-thread\ngesture that does not exist here there is nothing for them to drive. An\napp that imports one of these directly was reaching into upstream's\ninternals; the build failing at that import is the intended outcome.",
897
891
  },
898
892
  {
899
- doc: "docs/api.md",
900
- heading: "The boundary: what can be animated",
901
- text: "This is the honest limit of the surface, and it is not a runtime limit. What\nthis platform can write to a mounted widget without a React render:\n\n| Property | Reached through | How it reaches GTK |\n| ------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `opacity` | `useAnimatedStyle` | `gtk_widget_set_opacity`, straight from the animation frame. |\n| `transform` (`translateX/Y`, `scale`, `scaleX/Y`, `rotate`/`rotateZ`) | `useAnimatedStyle` | The rect store plus one queued allocation, applied as a `GskTransform`. No 3D, no skew, no `matrix` — the same list the static `transform` style takes. |\n| `top`, `left`, `right`, `bottom` — **only** on a node whose own `position` is `\"absolute\"` | `useAnimatedStyle` | Turned into a translate from the position the committed layout gave it: the same rect store, the same queued allocation, 1.99 µs. |\n| `width`, `height` — **only** where the change is confined to the node that owns it | `useAnimatedStyle` | The node's own subtree re-laid-out pinned to the driven value, into the rect store as an override — 7.1 µs for a leaf, 21.7 µs with wrapped text, the same at five siblings and at three hundred. |\n| `backgroundColor`, `color`, `borderColor` (and per side), `outlineColor` | `useAnimatedStyle` | A `GtkCssProvider` private to that widget, reloaded in place — 11.2 µs per frame, flat in the size of the tree. |\n| The numeric SVG props (`r`, `cx`, `strokeWidth`, `strokeDashoffset` and the rest of the geometry and paint numbers) | `useAnimatedProps` | The shape's own descriptor plus `queueDraw` — the SVG components subscribe to an animated node themselves, so nothing new writes. |\n\nColours deliberately do **not** go through the memoised class registry the\nstatic styles use. That registry keys on the generated CSS text, so a colour\ndriven through it would mint a class per frame into one process-wide\nstylesheet that GTK re-parses whole and that is never pruned — measured at\n0.8 ms for the first frame and 6.8 ms by the six-hundredth, still climbing.\nThe private provider has no cache and no document, so nothing about the\nstatic path — including its memoisation — changes. Every animated component\ngets this, not just `Animated.View`: the write path is a hook over \"a widget\nand its parent\", so `Animated.Text` and anything through\n`createAnimatedComponent` animate colours on the same terms.\n\n**The remaining layout properties are refused, and it is a decision rather\nthan a gap.** `flex`, `flexBasis`, every `margin*`/`padding*`, `gap` and the\n`min*`/`max*` family need a Yoga pass plus the commit walk that follows it,\nand that cost is proportional to the CONTAINER rather than to the animated\nvalue: 52 µs for a five-child container, 133 µs at sixty, 496 µs at three\nhundred, per frame. A transform is 1.5 µs at all three, and a colour 11.2 µs.\nA `useAnimatedStyle` that changes one of them warns once for that property,\nsays it is a layout property and why, and names the transform to use instead.\nThe value is still applied on the next React render rather than dropped — and\nwhen the value comes from an animation the updater returned\n(`height: withTiming(320)`), that render is produced for you: when the\nanimation reaches its target, and at most once per 100 ms while it is on its\nway, because otherwise there would not be one. Ten renders a second at the very\nmost, never one per frame: in a full run of `spike/core-exports`'s bottom\nsheet, 294 animation frames on refused properties produced 42 renders. The\ncadence is not decoration — with the settle alone, `@gorhom/bottom-sheet`\nmounting held its content mask at 96 px of a 954 px target for 1.38 s (its\ntarget is derived from the sheet's own position, so the opening spring re-aims\nit every frame and it never settles), and the list inside mounted none of its\nrows for all of it\n([research/animated-size.md §10](research/animated-size.md)).\n\nIt is a cost argument and only a cost argument. Two things that used to be\nsaid here were re-measured and are not true:\n[research/animated-size.md](research/animated-size.md) found that GTK\nre-measuring every ancestor after the resize adds nothing at any tree size,\nand that a size write cannot resize the window — the RN root reports a zero\nsize request, so the toplevel never re-negotiates. (An `IntrinsicRoot` mounted\ndirectly in GTK chrome does change the window's request, and is the one place\nthe hazard is real.)\n\n**`scaleX`/`scaleY` are an approximation for `width`/`height`, not a\nreplacement**, and the warning says which — it is the sentence a refused size\nstill gets. Measured on a 100×60 box widened to 260: a scale grows about the\nview's CENTRE, so the box moves as it grows (x 500 → 420, where the width\nchange kept x at 500), and it scales the CONTENT with the box instead of\nre-laying it out — the label inside kept its three-line 45 px layout and was\ndrawn stretched, where the width change re-wrapped it to one line of 15 px.\nReach for a scale when the content can take being stretched (a plain box, an\nimage). `translateX`/`translateY` for the insets are exact and carry no such\ncaveat.\n\nFull measurements in [research/animated-size.md](research/animated-size.md),\nnext to the original table in\n[research/animated-colors.md](research/animated-colors.md).\n\n#### The first exception: insets on an absolutely positioned node\n\n`top`, `left`, `right` and `bottom` **are** driven at frame rate, on a node\nwhose own `position` is `\"absolute\"`. Such a node is out of flow, so moving it\nchanges nothing but where it is drawn — which makes an inset exactly a\ntranslation from the position the committed layout gave it, and lets it run on\nthe transform path with no Yoga pass at all. It is the shape the whole\nsortable-list ecosystem is built on:\n\n```tsx\nconst style = useAnimatedStyle(() => ({\n position: \"absolute\",\n left: 0,\n right: 0,\n top: top.value, // driven — 1.99 µs, flat in the size of the list\n}))\n```\n\nFour things are worth knowing about it.\n\n- **It composes with your own transform**, it does not replace it. The derived\n translate is applied outermost, so it moves the already-rotated,\n already-scaled box by the distance the layout asked for — a `top: 100` under\n `scale: 2` moves the box 100 px, not 200.\n- **`right` and `bottom` invert**, because they measure inward from the far\n edge: a larger value moves the node towards the origin.\n- **An axis anchored by BOTH edges is still refused**, because it is no longer\n a translation. `left: 0, right: 0` with no `width` derives the width from\n both edges, so animating `left` there resizes the node; and with a definite\n `width` Yoga honours `left` and ignores `right` entirely, so animating\n `right` would invent motion a real layout pass would not produce. Both cases\n warn in their own words and say which configuration would work. (The\n sortable shape above is fine: its horizontal axis has two edges but neither\n of them animates, and its vertical axis has only `top`.)\n- **`measure()` reports the committed layout, not the translated position** —\n see below.\n- **`position` may live in a sibling style entry.** The usual spelling —\n `style={[styles.row, useAnimatedStyle(() => ({ top: y.value }))]}` — works:\n the decision is taken against the flattened style, not against the updater's\n object alone.\n\nMeasurements, including the hit-testing probe under real pointer injection and\nthe per-configuration table, are in\n[research/absolute-insets.md](research/absolute-insets.md).\n\n#### The second exception: a size that is confined to the node that owns it\n\n`width` and `height` **are** driven at frame rate where the change stops at\nthe node — the node's own subtree is re-laid-out pinned to the driven value,\nthe result goes into the rect store as an override, and one queued allocation\nputs it on screen. Nothing above the node is visited and nothing is written\ninto Yoga, so the cost is the size of the NODE rather than of its container:\n\n| children in the container | driven `width`, leaf | with wrapped text | the naive write | a transform |\n| ------------------------- | -------------------- | ----------------- | --------------- | ----------- |\n| 5 | 7.1 µs | 22.1 µs | 52.1 µs | 1.6 µs |\n| 60 | 6.9 µs | 21.8 µs | 133.1 µs | 1.5 µs |\n| 300 | 7.1 µs | 21.7 µs | 496.4 µs | 1.5 µs |\n\n```tsx\n// A progress bar, a disclosure panel, a sliding drawer — all the same shape.\nconst style = useAnimatedStyle(() => ({ width: width.value }))\n\n<View style={{ width: 400, height: 700 }}>\n {/* the container's width is its own, so nothing this box does can move it */}\n <Animated.View style={[{ height: 60 }, style]}>\n <Text>re-wraps as the box grows, which a scaleX does not</Text>\n </Animated.View>\n <View style={{ height: 20 }} />\n</View>\n```\n\n**This is a real layout, not a stretch.** The content inside is re-laid-out at\nthe new size: text re-wraps, a flex row inside redistributes, a stretched\nchild follows. That is the difference from `scaleX`, and it is why this is a\nYoga pass at all rather than one store write.\n\nThe precondition — measured, and refused where it does not hold:\n\n- **the axis is the container's CROSS axis.** A `width` in a column, a\n `height` in a row. A main-axis size pushes every following sibling along,\n which is the layout pass the whole refusal is about.\n- **the container's size on that axis does not come from its children.** A\n definite or percentage size, a `flex` from its own parent, or `stretch` on\n its parent's cross axis — the rule climbs until it finds one. A\n content-sized container would grow with the node.\n- **the node's OTHER axis does not come from its content.** A box with\n `height: auto` around wrapping text gets taller as it gets narrower, and\n everything after it moves.\n- **the node's resolved cross-axis alignment is `flex-start` or `stretch`.**\n `center` and `flex-end` move the node's own origin as it grows.\n- **no `aspectRatio`, and no `min`/`max` on that axis.** The first ties the\n other axis to this one; the second clamps the driven value, so the box\n silently stops following the animation.\n- **the container does not wrap**, which would re-size the node's line and\n move every line after it.\n- an **absolutely positioned** node qualifies too, on either axis, as long as\n the axis' START edge (`left`, `top`) is anchored — it then grows from an\n origin that does not move, and being out of flow it touches nothing at all.\n- **not under an `IntrinsicRoot`.** That root reports its Yoga content size to\n GTK, so a size below it feeds the window's own size request — and the driven\n value deliberately never goes into Yoga, so the island would keep its old\n request and the node would draw outside it. This is the one root shape where\n the original \"it can resize the window\" worry was real.\n\nEverything outside that keeps the refusal, and the warning names which of\nthose it was.\n\nThree more things are worth knowing.\n\n- **The container's `flexDirection` and `alignItems` are usually not in the\n updater's object** — `style={[styles.bar, useAnimatedStyle(() => ({ width:\nw.value }))]}` is the ordinary spelling. The decision is taken against the\n layout tree, so it sees the real answer either way.\n- **The driven size survives an unrelated engine flush.** It is kept as an\n override next to the animated offset rather than written over the committed\n rect, so a window resize — or any other reason the engine re-commits the\n tree mid-animation — cannot drop a frame of it.\n- **`measure()` reports the committed layout, not the driven size**, exactly\n as it does for a transform and for an animated inset. The node's Yoga size\n did not change; it catches up on the next React render.\n\nMeasurements, the hit-testing probe under real pointer injection, and the\nper-configuration comparison against a full layout pass are in\n[research/animated-size.md](research/animated-size.md).\n\n**`measure()` on a node moved this way reports the LAYOUT rect.** The node's\nYoga `top` did not change; only its allocated and painted position did. So\n`x`/`y`/`width`/`height` are the committed layout — untranslated — while\n`pageX`/`pageY` go through GTK's transform chain and report where the node is\nactually drawn. `measureInWindow` and `measureLayout` follow `pageX`/`pageY`.\nThis is a real difference from reading the geometry back on mobile, and it is\nthe same split an explicit `translateY` has always had here.\n\n**`zIndex` is driven, animated or not** — see item 10 of\n[the differences summary](#key-differences-from-react-native-summary) for what\nit means here and where it diverges. It is one widget\nwrite, no Yoga pass and no CSS, so it costs what `opacity` costs; the shape\n`useSortable` produces every frame (`{ position: \"absolute\", left: 0, right: 0,\ntop: top.value, zIndex: moving ? 1 : 0 }`) drives both `top` and `zIndex` and\nwarns about neither.\n\nEverything else — borders, radii, shadows — still reaches GTK as a CSS class\ncomputed during render. It is not dropped silently either: the property is\nnamed in a one-per-session warning and its latest value is applied on the\nnext React render — and when the value moves inside an animation, that render\nis produced for it (see above). `useAnimatedProps` has the same rule with the\nsame warning: a numeric prop is driven, anything else is named and lands on the\nnext render.\n\n#### An animation returned from the updater: where it starts, and both directions of a toggle\n\n`useAnimatedStyle(() => ({ height: withSpring(open.value ? 320 : 0) }))` is how\nReanimated's own documentation writes an animation, and it runs here on the\nplatform's one frame scheduler. Three rules decide what a given mapper run\ndoes with it, and all three are upstream's — read out of `styleUpdater` and\n`prepareAnimation` in `react-native-reanimated`'s `hook/useAnimatedStyle.ts`,\nnot inferred:\n\n- **A key animating for the first time is seeded at its target**, not animated\n to it. There is nothing to animate from. (Upstream arrives at the same place\n from the other side: its starting point for a key absent from the previous\n result is `undefined`, which leaves the animation's own `current` — the\n target — standing.)\n- **A key whose previous updater result held a plain NUMBER animates from that\n number.** So the common \"snap shut, open smoothly\" shape works, and both of\n these run 100 → 200 and 0 → 1 over the animation's duration rather than\n jumping:\n\n ```tsx\n useAnimatedStyle(() => ({ height: open.value ? withTiming(200) : 100 }))\n useAnimatedStyle(() => ({ opacity: visible.value ? withTiming(1) : 0 }))\n ```\n\n Upstream keeps the whole previous result and its starting-point branch for\n this case is one line: _\"previously it was a plain value, just set it as\n starting point\"_. This platform used to drop the key while it held the number\n and re-seed it when it next held an animation, so the seed went straight to\n the target and the animation did not play at all — silently, with the resting\n size correct\n ([research/animated-size.md §11](research/animated-size.md)).\n\n- **A later run producing the SAME animation does not restart it**, compared by\n target and shape rather than by object identity, because a mapper re-runs\n many times a second and every run builds a fresh descriptor.\n\n**The reverse direction is not the mirror image.** When a plain number replaces\na running animation — the `open.value = 0` half of the shapes above — the\nanimation is **cancelled and the number lands at once**; it does not ease back,\nand no settle is reported. That is upstream's behaviour too (it deletes the\nanimation and pushes the plain value in the same mapper run), so it is matched\nrather than symmetrised. On a driven property (`opacity`, `transform`, a\ncolour, an inset, a confined size) the number reaches the widget on that frame;\non a refused one the snap is a React render, and it is produced for you — a\nsnap that waited for the cadence or for a settle that will never come would\nleave the animation's old value on screen, which is the same silent failure\nthe cadence exists to prevent.\n\nA percentage or a colour string in the previous result is not a starting point\na numeric driver can use, so those fall back to the seed.",
893
+ doc: "docs/reference/dnd.md",
894
+ heading: "Differs from react-native-reanimated-dnd",
895
+ text: "The dragged view never moves. GDK carries a `Gtk.WidgetPaintable` of it above\nevery window, with the theme's own cursors and hit-testing against the real\nwidget tree — including widgets React Native never created. Everything below\nfollows from that one fact.",
902
896
  },
903
897
  {
904
- doc: "docs/api.md",
905
- heading: "Implemented",
906
- text: "| Export | Behaviour |\n| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `useSharedValue`, `makeMutable`, `isSharedValue`, `cancelAnimation` | Full. A shared value is also a platform animated node, so it can be handed to `Animated.View`'s style directly as well as through `useAnimatedStyle`. |\n| `useAnimatedStyle` | Full for `opacity`, `transform`, colours, the insets of an absolutely positioned node and a `width`/`height` whose change is confined to that node — see the boundary above. An animation RETURNED from the updater (`() => ({ opacity: withTiming(1) })`) runs here as it does upstream, on the platform's one frame scheduler; a key animating for the first time is seeded at its target rather than animated to it, a key whose previous updater result held a plain NUMBER animates from that number (`height: open ? withTiming(200) : 100` runs 100 → 200, upstream's rule), a plain number replacing a running animation cancels it and snaps rather than easing back (also upstream's, and not the mirror image of the previous rule), and a later run producing the same animation does not restart it. A style whose _shape_ changes between runs costs exactly one React render and rebinds; a running animation costs none, except on a REFUSED property, which is published through React when its animation settles and at most once per 100 ms while it runs — ten renders a second at the very most, against sixty frames. |\n| `useAnimatedProps` | Numeric props, driven straight into the component that takes them — in practice the SVG shapes, which already accept `number \\| AnimatedNode` on every geometry and paint number. Same lifecycle as `useAnimatedStyle`, down to the one render a shape change costs. |\n| `useDerivedValue`, `useAnimatedReaction`, `startMapper`, `stopMapper` | Full. Mappers are torn down on unmount. |\n| `withTiming`, `withSpring`, `withSequence`, `withRepeat`, `withDelay` | Full for numeric values, on upstream's defaults (timing 300 ms / `inOut(quad)`, spring `GentleSpringConfig`), driven by the platform's own frame scheduler. Assigned to a shared value or returned from a `useAnimatedStyle`/`useAnimatedProps` updater, both. One divergence: re-aiming a running animation keeps its current value but takes the new descriptor's velocity, where upstream also carries the previous animation's velocity across — a target that moves every frame is therefore slightly more damped here. |\n| `withDecay`, `withClamp` | Full, including `velocity`, `deceleration`, `velocityFactor`, `clamp` and `rubberBandEffect` — upstream's own step function, ported. `withDecay` is what an inertial fling rides on: released with a velocity, it coasts, decelerates and stops with no target. `withClamp` runs its inner animation un-truncated and clips what reaches the value, which is upstream's distinction and is observable on an overshooting spring. |\n| `interpolate`, `clamp`, `Extrapolation`, `Extrapolate`, `Easing` | Full, including per-edge extrapolation and `Easing.bezier`'s factory shape. |\n| `interpolateColor`, `convertToRGBA`, `isColor`, `rgbaArrayToRGBAColor` | Full for `'RGB'` (upstream's 2.2 gamma) and `'HSV'` (upstream's hue-wrap correction), including its `transparent` handling. `'LAB'` throws — see the differences table. |\n| `PlatformColor` | The platform's own: theme colours by name, resolved by GTK against the live Adwaita palette. Can be animated _between_ on a shared value; cannot be interpolated _through_. |\n| `useAnimatedScrollHandler` | `onScroll` in full, on a path that was already there: `ScrollView`'s `emitScroll` runs from a `GtkAdjustment::value-changed` handler — a C callback on the loop this JS is on — so a handler that writes a shared value gets Reanimated's promise (no React render per scroll) without any event machinery. Hand the result to a scrollable's `onScroll`; the handler receives Reanimated's FLATTENED event (`event.contentOffset.y`, not `event.nativeEvent`) carrying the three measurements a `GtkScrolledWindow` can report, plus one context object shared by every call. `onBeginDrag`/`onEndDrag`/`onMomentumBegin`/`onMomentumEnd` are called: a wheel burst gets one synthetic begin/end pair and no momentum, while a touchpad glide gets all four from its native sequence. The wheel pair is a documented desktop extension because RN has no wheel. Routed through the ONE `onScroll` prop, as on mobile — `@gorhom/bottom-sheet` passes no phase prop at all — and sharing the single context object with `onScroll`, which is what its scroll lock is built on. `contentInset`, `velocity` and `zoomScale` are absent from the event rather than invented as zeros. See the `ScrollView` row and [research/scroll-phases.md](research/scroll-phases.md) |\n| `scrollTo` | `scrollTo(ref, x, y, animated)` on the scrollable an `useAnimatedRef` points at — the write half of the hook above, and the same reasoning: this IS the thread that owns the widget, so it calls the scrollable's own imperative `scrollTo` synchronously. Upstream's argument order rather than RN's options object, so library call sites are unchanged; `animated` is ignored, as it is on `ScrollView`. A ref pointing at nothing (or at something without a scroll API) is ignored rather than throwing, as upstream's is. |\n| `useScrollOffset`, `useScrollViewOffset` | A shared value holding a scrollable's current offset, updated from the adjustment's own `value-changed` — no `onScroll` prop needed and no React render. Takes upstream's second argument (write into a shared value you already own) and upstream's own axis rule (`x` when there is a horizontal offset, `y` otherwise). Point it at a `ScrollView`, a `FlatList` (which resolves through to the `ScrollView` it renders, as `findNodeHandle` does) or an `Animated.ScrollView`; a ref on anything else warns once and the value stays 0. Costs 5.15 µs per scroll event while tracking and nothing at all while not — it connects on mount and disconnects on unmount. |\n| `useHandler` | Full. `doDependenciesDiffer` is always **false**, and that is a statement rather than a stub: upstream needs it because a worklet is a by-value snapshot that goes stale, and here a handler is an ordinary closure read out of a ref at call time. `useWeb` is true, for the reason the whole surface is on upstream's web path. |\n| `useEvent` | **Scroll event names only** — `onScroll`, `onScrollBeginDrag`, `onScrollEndDrag`, `onMomentumScrollBegin`, `onMomentumScrollEnd`. The value it returns goes on a scrollable's `onScroll`, which IS the subscription here; that is the same object `useAnimatedScrollHandler` returns, so a hand-built handler and the stock one behave identically. Any other event name throws where it is asked for, naming itself: there is no native event registry to subscribe an arbitrary name against, and a subscription that could never fire is the failure mode this package refuses everywhere else. `rebuild` is accepted and ignored, for the same reason `doDependenciesDiffer` is false. `.workletEventHandler` throws — it registers a native view TAG, and there is neither. |\n| `useAnimatedKeyboard`, `KeyboardState` | Real shared values, **honoured and never updated** — the same shape and the same reason as RN's `Keyboard`: every number the hook reports describes a software panel taking screen space from the app, and a desktop has none. `height` is 0 because the keyboard occupies nothing and `state` is `CLOSED` because it is — deliberately not `UNKNOWN`, which upstream seeds only until the native side reports and here would be false. A `useAnimatedStyle` reading them subscribes, computes and settles once, so an app written for three platforms keeps one source and gets the right answer here. |\n| `useAnimatedRef`, `measure` | Full, and callable from anywhere — there is no worklet to be inside of. Returns `null` before the first committed layout, which is RN's own contract. |\n| `runOnUI`, `runOnJS`, `scheduleOnUI`, `scheduleOnRN` | Deferred, not inlined — see below. |\n| `Animated.View` | The platform's own, unchanged. Takes a `ref` giving `measure`/`measureInWindow`/`measureLayout`. |\n| `Animated.Text`, `Animated.Image`, `Animated.ScrollView` | `createAnimatedComponent` over the platform's own components — no subclass and no special case. All three forward the `ref` through, so `useAnimatedRef` + `measure()` works on them. |\n| `createAnimatedComponent` | **Adds no widget to the tree.** It renders the wrapped component itself and reaches its widget through the ref that component already exposes, so the GTK output is what the unwrapped component produces. Wrap anything that takes a `ref` giving the geometry methods; anything else gets a named warning rather than a silent no-op. |\n| `entering`, `exiting`, `layout` | On every animated component, not only `Animated.View` — see the layout-animation section below. `exiting` keeps the widget on screen after React has removed it. |\n| `FadeIn`, `FadeOut`, `LinearTransition`, `Layout`, `Keyframe` | Upstream's fluent surface (`.duration()`, `.delay()`, `.easing()`, `.springify()` and the spring parameters, `.rotate()`, `.withInitialValues()`, `.withCallback()`), usable as the class or as an instance. `Layout` is upstream's own deprecated alias of `LinearTransition`. |\n| The preset catalogue: `Fade*`, `Bounce*`, `Pinwheel*`, `Roll*`, `Rotate*`, `Slide*`, `Stretch*`, `Zoom*` | 60 of upstream's 76, on upstream's own parameters, sharing one builder over a table. The 16 that are refused are the twelve `Flip*` (`perspective` + `rotateX`/`rotateY` — a 3D rotation, where this platform folds a transform array into one 2D affine matrix) and the four `LightSpeed*` (`skewX`, a deliberate cut across the whole transform surface). Both throw by name. |\n| `CurvedTransition`, `FadingTransition`, `JumpingTransition`, `SequencedTransition`, `EntryExitTransition` | The four `layout` transitions beside `LinearTransition`, plus the one that composes an entering and an exiting builder into a single layout animation. Same properties, same paint-only position (see the differences table). |\n| `LayoutAnimationConfig`, `enableLayoutAnimations` | `<LayoutAnimationConfig skipEntering skipExiting>` suppresses the animations of the subtree below it, and adds no widget. `enableLayoutAnimations` warns and does nothing, which is exactly what it does upstream — it is deprecated there and its allow-list is gone. |\n| `withReanimatedTimer`, `advanceAnimationByTime`, `advanceAnimationByFrame` | Real, and not an emulation: the frame driver every animation here runs on is this platform's own, so a test takes it and steps it. `withReanimatedTimer` also accepts an async body, which a `@gtkx/testing` test needs. `getAnimatedStyle` and `setUpTests` are refused — see the differences table. |\n| `BaseAnimationBuilder`, `ComplexAnimationBuilder` | One class under both names — upstream splits the plain chain from the spring parameters, this platform does not — so a library subclassing either keeps working. |\n| `GentleSpringConfig` and the other seven spring presets, `ReduceMotion`, `ReanimatedLogLevel`, `isSharedValue` | Plain data, mirrored exactly. |\n| `isConfigured`, `isReanimated3`, `makeShareableCloneRecursive`, `isWorkletFunction`, `configureReanimatedLogger` | Present. Cloning is identity (nothing leaves the runtime it was made in); `configureReanimatedLogger` is accepted and does nothing, because there is no second logger to configure. |\n\nAnimating an SVG shape is the case `useAnimatedProps` exists for, and it\nreads exactly as it does on mobile:\n\n```tsx\nimport { Circle, Svg } from \"react-native-gtkx/svg\"\nimport Animated, {\n useAnimatedProps,\n useSharedValue,\n withTiming,\n} from \"react-native-reanimated\"\n\nconst AnimatedCircle = Animated.createAnimatedComponent(Circle)\n\nconst Pulse = () => {\n const r = useSharedValue(10)\n const animatedProps = useAnimatedProps(() => ({ r: r.value }))\n return (\n <Svg\n width={100}\n height={100}\n onLayout={() => (r.value = withTiming(40))}\n >\n <AnimatedCircle\n cx={50}\n cy={50}\n fill=\"green\"\n animatedProps={animatedProps}\n />\n </Svg>\n )\n}\n```\n\n#### Writing a shared value: `.value` or `.set()`\n\nBoth spellings are upstream's and both work here: `sharedValue.value = x`, or\n`sharedValue.get()` / `sharedValue.set(x)` (which also takes an updater,\n`count.set((current) => current + 1)`). They differ only under lint. The\nReact Compiler — [on by default on the vite path](getting-started.md#the-react-compiler-is-on-by-default-vite-path) —\ntreats anything a hook returns as frozen, so `react-hooks/immutability`\nreports **every** assignment to `.value`, including the ones inside a\ncallback or an effect that are perfectly legitimate. `.get()`/`.set()` is\nwhat upstream added for exactly this case and lints clean everywhere. Prefer\nit in new code; `.value` keeps working, and a ported app does not have to be\nrewritten.",
898
+ doc: "docs/reference/dnd.md",
899
+ heading: "The drag layer",
900
+ text: "A dragged `Draggable`/`SortableItem` escapes any `overflow: hidden` ancestor\nautomatically — not a prop, the same way GDK's own drag icon is not one.\nGDK's icon already escapes any clip in this process's own tree (it is a\ncompositor surface, not a descendant of anything here), but that is only a\ncue at the cursor this process cannot introspect. While a drag is in flight,\na second, non-interactive `Gtk.Picture` showing a live `Gtk.WidgetPaintable`\nof the dragged row is added to a `Gtk.Overlay` wrapped once around each\nwindow's real content, escaping every ancestor's clip the same way any\n`Overlay` child does.\n\nThe original view dims to reduced opacity for the drag's duration — restored\nto whatever it was, not hardcoded — rather than disappearing, the same\npattern `react-native-draggable-flatlist`'s `activeOpacity` and similar\nlibraries use. Because a `Gtk.WidgetPaintable` is a live view of the widget\nit observes, GDK's own drag icon and this overlay copy dim along with the\noriginal — the three are one underlying render.\n\nThe dragged widget itself is never reparented into the overlay: a 100×100\ncard would render at 800×600 under a new parent's own size negotiation, and\nan unmount mid-drag would strand the widget outside the tree React still\nowns. The overlay copy takes no input (`can-target: false`); neither\nhit-testing nor the responder path changes, both still resolve against the\noriginal widget unchanged. See [Gestures](../architecture/gestures.md) for the\nresponder path itself.\n\nZero React renders happen per frame: positioning the overlay copy is two\nwidget property writes (`setMarginStart`/`setMarginTop`) per motion event,\nabout 1.76 µs median.",
907
901
  },
908
902
  {
909
- doc: "docs/api.md",
910
- heading: "Layout animations, and the one primitive they needed",
911
- text: "```tsx\n<Animated.View\n entering={FadeIn.duration(300)}\n exiting={FadeOut}\n layout={LinearTransition.springify()}\n/>\n```\n\nAll three work on every animated component`Animated.View`,\n`Animated.Text`, `Animated.Image`, `Animated.ScrollView` and anything through\n`createAnimatedComponent` because they are added by wrapping rather than by\nsubclassing, and the wrapper adds no widget to the tree any more than\n`createAnimatedComponent` does.\n\n`entering` and `layout` needed nothing new. `entering` writes the builder's\ninitial values in the commit that mounts the widget (so it is never drawn\nun-faded, not even for a frame) and animates from there. `layout` watches for\nthe layout engine committing a **different rect** for that child and walks it\nfrom where it was to where the engine put it.\n\n**`layout` animates the position, and applies the size.** Upstream's\n`LinearTransition` animates `originX`/`originY`/`width`/`height`; all four are\nstill produced here, and the origins are honoured as a **translation** the\nsame paint-only write a `transform` uses, composed with whatever transform the\nstyle already has, so a row that scales while the list reorders does both. A\nsize change lands immediately instead, for the reason the boundary section\nabove already gives: animating a size means a Yoga pass per frame whose cost\nis the tree's rather than the animated value's.\n\n**`exiting` is the one that needed a new primitive**, and it is the reason\nthis slice exists. An exit animation has to keep drawing a widget React has\nalready reconciled away, and React's deletion is neither asynchronous nor\nnegotiable: in one synchronous commit it runs the unmounting subtree's\ncleanups and unparents its topmost widget. So the platform grew a **widget\nretention** primitive, generalised from the one\n`react-native-gtkx/adw`'s `NavigationStack` already used for pages hold what\nis leaving, drop it on the real end signal, and arm a timer in case that\nsignal never comes:\n\n- The widget is put back into the same container, **at the end of the child\n list**, so it draws over the siblings closing the gap rather than under\n them.\n- Its Yoga node leaves the shadow tree immediately, so an exiting view does\n **not** hold its space the row below it moves up at once, and the fade\n happens over the top.\n- Every container in the retained subtree keeps its layout manager until the\n animation ends, so the exiting view's own children stay exactly where the\n engine put them.\n- **A fallback timer always runs**, armed from the animation's declared\n length. Whichever arrives first the animation's end or the timer — drops\n the widget, so a spring that never settles, a frame source that dies, or an\n animation that was never started cannot leak a widget that is still\n parented, drawn and hit-testable.\n\n`exiting` is skipped when the component's own container is unmounting in the\nsame commit: there is no container left to hold the widget, and an exit\nanimation inside a disappearing parent is not one anybody sees.",
903
+ doc: "docs/reference/dnd.md",
904
+ heading: "Prop-by-prop",
905
+ text: "- **`preDragDelay`** — Accepted, ignored. GDK's `gtk-dnd-drag-threshold`\n already separates a tap from a drag.\n- **`collisionAlgorithm`** Accepted, ignored. GDK hit-tests the pointer\n directly; `\"center\"` is the closest of the three algorithms to that.\n- **`requestPositionUpdate()`** A no-op. Nothing caches a slot\n rectangle, because GDK re-hit-tests every motion event.\n- **`onLayoutUpdateComplete`** Accepted, ignored there is no layout\n pass to complete.\n- **`itemHeight`, `estimatedItemHeight`, `enableDynamicHeights`,\n `useFlatList`, `containerHeight`, `containerWidth`** Accepted, ignored.\n Yoga lays rows out at their natural height, and the mirror's own\n `ScrollView` measures its own viewport for autoscroll rather than\n trusting a hint.\n- **`dragAxis`, `dragBoundsRef`, `animationFunction`** **Unsupported.**\n All three describe where the dragged view goes, and it never goes\n anywhere here. Kept in the type so a file shared with iOS and Android\n still compiles.\n- **`dropAlignment`, `dropOffset`** **Unsupported**, same reason.\n- **`positions`, `lowerBound`/`leftBound`,\n `autoScrollDirection`/`autoScrollHorizontalDirection`, `itemHeights`** —\n Real `{ value }` boxes (`SharedValueLike`), not `SharedValue`.\n Forwarding them with `{...rest}` works, reads work;\n `autoScrollDirection`/`autoScrollHorizontalDirection` are genuinely\n written by the built-in autoscroll (below), the rest do not animate.\n- **`SortableDirection.Horizontal`, `useHorizontalSortable`,\n `useHorizontalSortableList`** Implemented. Reorder-by-crossing does\n not care which axis a list scrolls along the tracked position reads\n whichever coordinate the axis cares about so this is\n `Sortable`/`useSortable`'s own machinery with a horizontal `ScrollView`\n and `leftBound`/`autoScrollHorizontalDirection` plumbing, not a second\n implementation. `gap`/`paddingHorizontal` are real Yoga layout on the\n content container, not hints.\n- **`SortableGrid`, `SortableGridItem`, `useGridSortable`,\n `useGridSortableList`** Implemented. The grid is a real Yoga\n `flexWrap` layout fixed-size cells, a fixed cross-axis dimension\n (`columns`/`rows` × `itemWidth`/`itemHeight`) rather than upstream's\n absolutely-positioned cells at a `useAnimatedStyle`-computed `top`/\n `left`; the same row/column arithmetic (`calculateGridPosition`) places\n them, a different engine paints it. `getGridCellFromCoordinates` floors\n onto the cell whose top-left corner is at or before a point, exactly\n matching upstream. `SortableGridItem`'s `isBeingRemoved` removal\n animation is accepted and ignored, same reason as `animationFunction`\n above. `scrollEnabled` is accepted and ignored too — this platform's\n `ScrollView` has no prop to disable input the way upstream's does.\n- **Autoscroll near a container edge during a drag** Implemented for\n `Sortable` and `SortableGrid`: a `GtkDropControllerMotion` on the list's\n own viewport reports how close the drag sits to an edge, and a\n `Gtk.Widget` tick callback nudges the real `GtkAdjustment` toward it for\n as long as it stays there an imperative per-frame write, no React\n render either way. One difference from upstream: the scroll runs at a\n constant speed while the edge band is occupied, rather than easing into\n a 1500ms glide, because there is no timing engine here to ease with. Not\n wired into the standalone\n `useSortableList`/`useHorizontalSortableList`/`useGridSortableList`\n hooks, which build no `ScrollView` of their own to drive.\n- **Sortable list height** Rows are in flow layout, so the list is as\n tall as its rows, not `itemsCount × itemHeight`.",
912
906
  },
913
907
  {
914
- doc: "docs/api.md",
915
- heading: "Differences from `react-native-reanimated`",
916
- text: "| Behaviour | Here |\n| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Animatable properties | `opacity`, `transform` and colours — see the boundary table. Layout properties are refused with the transform to use instead; anything else warns once by name. Both land on the next render. |\n| Animated values | Numbers, and plain objects/arrays whose leaves are numbers — upstream's real `AnimatableValue`, minus colour strings: `withTiming(\"#ff0000\")` still throws rather than animating nothing, since colours go through `interpolateColor` instead, which is what upstream's own examples do too. `withTiming({x: 10, y: 20})`/`withSpring` interpolate every leaf on the same curve/clock (a nested object recurses; an array's own elements are always numbers, never nested — upstream's own asymmetry, `reanimated-compat/animatable-value.ts`), the callback fires once per animation rather than once per leaf, and composition through `withDelay`/`withSequence`/`withRepeat` carries a shape exactly as it carries a number. A target whose shape does not match the value it is animating from throws, naming the leaf, rather than silently dropping the mismatched key the way upstream's own from-value-driven walk does. A key that previously held a plain NUMBER seeds an animation from it (unchanged); a key that previously held a plain OBJECT does not — upstream's `prepareAnimation` has no branch for a plain data object either, so it is seeded at the target exactly like a key that was absent. Per-frame cost of an `{x, y}` `withTiming`: about 2× a single number's own frame (0.24 µs against 0.12 µs, VM-measured median of 15 × 100,000 frames, `spike/bench-vector-animated-values.ts`) — before the result ever reaches a style property, which is what the boundary table above actually costs. |\n| `interpolateColor` colour spaces | `'RGB'` and `'HSV'`. `'LAB'` throws by name — upstream's is a vendored slice of culori fed 0-255 channels where culori documents 0-1, so matching it would mean matching the scaling. |\n| `interpolateColor` inputs | Colour strings only, and not `PlatformColor` — a theme colour has no value until GTK resolves it against the live theme, so it has nothing to blend. Both cases throw and say which one happened. |\n| `processColor` | Throws. It returns RN's packed AARRGGBB integer, whose only consumer is a native module; a colour's destination here is a GTK stylesheet, which takes strings. |\n| `runOnUI` / `runOnJS` | Schedule rather than run inline, and return `void`, as upstream. A UI hop is a **task**, an RN hop a microtask — so a UI hop is still the later of the two, but it does not wait for a frame the way upstream's _web_ build does (`requestAnimationFrame` stands in there for a UI runtime the web has not got; React Native's real one does not wait either). Waiting cost a `scheduleOnUI(measure)`/`scheduleOnRN(use it)` round trip a whole frame, which is longer than the gap between two GTK pointer events — see `docs/research/dnd-hover-flicker.md`. |\n| `SharedValue.addListener` | Accepts upstream's `(listenerID, listener)` **and** this platform's animated-node `(callback) => id`. Both callers are real, and supporting only one fails silently. |\n| Worklet closure capture | Live lexical capture, not the plugin's by-value snapshot. Only observable for a worklet closing over a reassigned plain `let`, which is already a bug on mobile. |\n| `withSpring` rest condition | Upstream stops on remaining energy relative to initial energy; the platform's solver stops on displacement and speed thresholds, derived here from the same energy budget. The stopping point differs by well under a pixel. |\n| `withDecay` config validation | Throws at the `withDecay()` call rather than on the animation's first frame. Same errors (`clamp` shape, `velocityFactor > 0`, `rubberBandEffect` needing a `clamp`), one line earlier. |\n| `ReduceMotion`, `useReducedMotion` | The enum is mirrored and every value behaves as `Never`; `useReducedMotion()` is always false. GNOME's `gtk-enable-animations` is not read yet. |\n| `reanimatedVersion` | The upstream version this surface mirrors, not a claim to be that package. |\n| `LinearTransition` size changes | The position animates (as a translation); a width or height change lands immediately. A layout transition animates a rect the ENGINE produced, so it has no style to pin a subtree to — the carve-out `useAnimatedStyle` has for a driven `width`/`height` does not reach it. |\n| Layout-animation properties | `opacity`, `transform` and position. `width`/`height` are applied rather than driven (above); anything else a builder asks for is named once, by property, in a warning. |\n| Builder methods | `.restDisplacementThreshold()` and `.restSpeedThreshold()` are accepted and ignored — this platform's spring derives its rest condition from the same energy budget instead (see the row above). `.reduceMotion()` is accepted and ignored for the reason `useReducedMotion()` is always false. |\n| `entering` / `exiting` ownership | A layout animation owns `opacity` and `transform` for as long as it runs, so a `useAnimatedStyle` driving the same property on the same view during a fade is two writers on one slot. Upstream has the same rule. |\n| The layout-animation catalog | 60 of the 76 presets, all five `*Transition` builders, `Keyframe` and `LayoutAnimationConfig`. `Flip*` needs a 3D rotation and `LightSpeed*` a skew; both throw by name. `rotate` is carried as degrees rather than upstream's `'90deg'`/`'5rad'` strings — a numeric animation cannot carry a unit, and the matrix that reaches GTK is identical. A builder's own `.rotate()` and a `.withInitialValues()` angle still take either spelling. |\n| `CurvedTransition` size easings | `.easingWidth()` and `.easingHeight()` are accepted and ignored, for the reason in the `LinearTransition` row above: a size change lands immediately rather than being driven, so there is no curve to apply to it. The two position easings are honoured. |\n| `SharedTransition` | Throws. It needs three things that do not exist here: a `sharedTransitionTag` prop, an overlay above the navigation stack, and a retention that REPARENTS the leaving widget rather than holding it in place. Upstream's own web path does not implement it either. |\n| `getAnimatedStyle`, `setUpTests` | Throw. Upstream returns the style object its updater produced, which exists on mobile because its Jest path mirrors it onto the component; here a style is taken apart at bind time — opacity to the widget, colours to a private CSS provider, the whole `transform` array folded into one matrix — so there is no such object to return. Assert the widget instead (`getOpacity()`, `computeBounds()`), which is what every GTK test in this repo does and is strictly stronger. |",
908
+ doc: "docs/reference/dnd.md",
909
+ heading: "Reorder feel: how a crossing resolves",
910
+ text: "`Sortable`/`SortableGrid` track the dragged item's own rect the same way\nupstream does — `fromIndex * slotSize` plus the pointer's delta since the\ndrag began, reusing the same `GtkDropControllerMotion` the edge-autoscroll\nabove already watches every motion event with.\n\nDiffers from react-native-reanimated-dnd: this mirror resolves which slot\nthe item has landed on by rounding that tracked position rather than\nflooring it — the dragged item's center against a slot's center, not its\ntop-left corner against the slot's origin — symmetrically in both\ndirections. Measured with a real pointer, a 100px row or cell needs about\n50–60px of travel either way — away from index 0 or toward it, a center grab\nor an edge grab — before the crossing resolves.\n\nThe real, unaliased `react-native-reanimated-dnd` package's own arithmetic\nfloors the dragged rect onto a slot boundary from its top-left corner\ninstead: crossing a neighbor toward index 0 takes about one pixel of\ntravel there, crossing one away from it takes the neighbor's entire size in\nthat axis. That asymmetry is upstream's own behavior, reproduced unchanged\nwhen an app opts out of the mirror and runs the real package — not a\ncompat-surface distortion this platform introduces.\n\nThe origin the tracking measures against is the drag's own grab point,\nconverted to the list's container coordinates — never the first motion\nsample after a drag begins, since under fast pointer motion that sample can\nalready be displaced past GDK's own drag-start threshold, which would\nsilently undercount every reading taken from it. The change is scoped to\n`Sortable`/`SortableGrid`'s own reorder mechanism; `Draggable`/`Droppable`'s\ndrop-zone hit-testing is untouched and still GDK's own — `collisionAlgorithm`\nstays accepted-and-ignored there, as above.\n\nPer-motion-event cost of the tracking arithmetic itself is about 0.003 µs\nmedian (settling from about 0.01 µs on the first JIT round) — pure\narithmetic, no FFI hop — next to the drag layer's own roughly 1.76 µs for\nits two real GTK property writes per motion event.",
917
911
  },
918
912
  {
919
- doc: "docs/api.md",
920
- heading: "Not implemented — throws, naming itself",
921
- text: "`Animated.FlatList`; sixteen of the seventy-six preset layout-animation\nbuilders — the twelve `FlipIn*`/`FlipOut*` (a 3D rotation with a perspective)\nand the four `LightSpeed*` (a skew), both of which need a transform this\nplatform's 2D matrix has no room for;\n`processColor` and `DynamicColorIOS`; `useComposedEventHandler`,\n`useFrameCallback`, `useTimestamp`; sensors, screen and shared-element\ntransitions; Reanimated 4's CSS animations (`css.create`, `css.keyframes`);\n`defineAnimation`; `createWorkletRuntime` and `runOnRuntime` (see the worklets\nsection below); `getAnimatedStyle` and `setUpTests` (the other three test\nhelpers are implemented).\n\n**Why `Animated.FlatList` is a decision and not an omission.** It is the one\nanimated component that is refused, because it is a _composite_ rather than a\nhost component: `FlatList` renders the windowed core, which renders a\n`ScrollView`, which is the only thing in that chain that owns a widget and\n`FlatListHandle` is a scroll API by contract, so there is no handle to read a\nwidget back out of. Giving it one would mean publishing the scrolled window\nthrough two layers whose job is to hide it. Upstream's `Animated.FlatList`\nmostly exists so `onScroll` can be an `Animated.event` /\n`useAnimatedScrollHandler` and the second of those IS implemented here, so\na plain `FlatList` already takes one on its `onScroll` and needs no animated\nwrapper for it. Put the animated style on an `Animated.View` around the list,\nor use `Animated.ScrollView` when the list does not need virtualization.\n\nThe throw is the point, and it is the same discipline as the RNGH shim: a\n`BounceIn` that mounted without bouncing is the trap\n[research/gestures.md](research/gestures.md) records `Animated.View` falling\ninto — compiled, ran, did nothing. The stand-ins fail on call, on render and\non property access (`BounceIn.duration(300)`, `css.create`), while still\nanswering the introspection React and `console.log` do first. A symbol not\nlisted at all fails earlier still, at bundle time.\n\n**`@gorhom/bottom-sheet`'s scroll lock runs**, and it took two fixes a layer\nbelow this surface to get there. Both halves of the lock were always here —\nthe `onScroll`/`onBeginDrag`/`onEndDrag`/`onMomentumEnd` handlers it registers,\nand the `scrollTo` they call to pin the list — and driving the sheet with a\nreal pointer showed it never executing, because the sheet's scrollable emitted\nno scroll event at all.\n\nThe first cause was the scrollable's own style, and the base style above is it:\n`<FlatList />` carrying no style of its own inside a BOUNDED parent now scrolls\n— `spike/core-exports` measures it beside its controls, row one moving 406 →\n278 where it used to report 170 → 170.\n\nThe second was that gorhom's parent was not bounded, and the diagnosis on file\nfor that one was **wrong**, which is worth keeping because it was wrong in an\ninstructive way. gorhom bounds the list with an animated `height`\n(`contentMaskContainerAnimatedStyle` in `BottomSheetContent`), and the blame\nwent to the driven-size carve-out: a size that lives as a rect-store override\nYoga never sees cannot bound a child. The probe said otherwise — the height\nnever became a **number**. `useAnimatedStyle` did not run animations returned\nfrom its updater, so the property arrived as a spring descriptor and the\ndriven-size path was never even asked. It runs them now, and a `height` this\nplatform refuses to drive at frame rate lands in Yoga through a React render\nwhen its animation settles and at most once per 100 ms while it runs — 42\nrenders against 294 animation frames in a measured run. The settle on its own\nshipped first and was not enough: gorhom's mask target is derived from the\nsheet's own position, so the opening spring re-aims it every frame and it never\nsettles, and the sheet stood empty for 1.38 s\n([research/animated-size.md §9 and §10](research/animated-size.md)).\n`spike/core-exports` reaches 0 FAILED: the list receives scroll events, the\nlock holds it at the top while the sheet is collapsed and releases when it is\nextended.\n\n**`@gorhom/bottom-sheet` and `react-native-draggable-flatlist` both run now**,\nand this surface is one of the three they needed: the other two are\n[`react-native-gesture-handler`](#react-native-gesture-handler-react-native-gtkxgesture-handler)\nand four `react-native` core exports. What each of them was actually stopped\nby — and how that was established — is in\n[the section that measured it](#the-two-libraries-this-surface-was-measured-against-run).",
913
+ doc: "docs/reference/gesture-handler.md",
914
+ heading: "GestureHandlerRootView",
915
+ text: "`GestureHandlerRootView` renders a `View` with `style ?? { flex: 1 }` the\nsame default upstream's three platform implementations agree on. An explicit\n`style` prop **replaces** the default box rather than merging with it: an app\nthat passes `style={{ height: 100 }}` gets a 100px box with no `flex`, not a\nflexing one with a height added on top.\n\nUpstream's root view has a second jobmarking the subtree as\ngesture-arbitrating that this platform does not need to reproduce: the\nresponder system's lock is already global, so there is no scope for a\nprovider to draw. `GestureHandlerRootView` is therefore a plain layout box,\nfaithful to upstream's rendered output, with nothing else attached to it.\n\nAn app places one at the root of its tree, as upstream's own documentation\nrecommends, so that anything relying on the default `flex: 1` to fill the\nscreen has it.",
922
916
  },
923
917
  {
924
- doc: "docs/api.md",
925
- heading: "`react-native-worklets` (`react-native-gtkx/worklets`)",
926
- text: "Reanimated 4 moved the worklet surface out of Reanimated and into its own\npackage, and libraries import it under that name. Aliasing\n`react-native-reanimated` alone therefore left the import wall standing one\npackage over and it is an **import**-time wall, not a runtime one:\n`react-native-reanimated-dnd` 2.0.0 pulls `scheduleOnRN` and `scheduleOnUI`\nout of `react-native-worklets` at module scope in five of its hooks\n(`useDraggable`, `useDroppable`, `useSortable`, `useHorizontalSortable`,\n`useGridSortable`) with no `try { require } catch` anywhere, so the module\nfails to load rather than failing where the function is used. Both presets\nalias the package name onto this subpath, so an app keeps its source.\n\nTwo measurements worth keeping, taken against the published packages rather\nthan their docs:\n\n- **`react-native-reanimated-dnd` 2.0.0 imports exactly two symbols from it**\n `scheduleOnRN` and `scheduleOnUI`, both implemented here.\n- **`@gorhom/bottom-sheet` 5.2.14 imports none.** It reaches `runOnJS` and\n `runOnUI` through `react-native-reanimated`, and does not depend on\n `react-native-worklets` at all. What blocks it is measured in the\n `react-native-gesture-handler` section, and it is not this package.\n `react-native-gesture-handler` 3.1.0 does use this package\n (`scheduleOnUI`), but behind a `try { require } catch`, so it never had this\n failure mode.\n\nThe thread functions here and the ones `react-native-gtkx/reanimated` exports\nare the **same instance**, not two copies: jobs queued through either package\nname land in one batch, in order, exactly as upstream, where Reanimated\nre-exports them from this package.",
918
+ doc: "docs/reference/gesture-handler.md",
919
+ heading: "GestureDetector",
920
+ text: "![The gallery's Gesture detector section: Pan, Tap and LongPress recognizers configured with activateAfterLongPress, activeOffset/failOffset, hitSlop and numberOfTaps.](../shots/gallery/gesture-detector.png)\n\n`GestureDetector` renders exactly one child and adds no widget of its own. It\nreaches the child's underlying GTK widget through the same ref-forwarding\nseam `createAnimatedComponent` uses, and merges its recognizer's responder\nprops into the child's own a child with its own `onTouchStart` keeps\nworking alongside the gesture. Passing a fragment, a string, or more than one\nchild throws, naming the requirement, because there is nothing for a second\nwidget to attach to. Passing something that is not a gesture spec — not built\nwith `Gesture.*()`, a hook, or a composer throws as well, naming the\nmethods that do produce one.\n\nIf the child does not forward a ref to a widget-backed component at all an\nopaque wrapper that renders, say, an `Animated.View` internally without\nforwarding its own ref or unknown props onto it `GestureDetector` falls\nback to a context-based attachment instead: one of this platform's own\ncomponents mounted somewhere inside that child can claim the gesture on its\nown widget. This exists because `react-native-sortables`' v3 gesture-handler\nintegration hands `GestureDetector` exactly such a wrapper.\n\n`hitSlop`, `shouldCancelWhenOutside`, and the `x`/`y` fields on every payload\nare all measured against the gesture's own view — which is why the widget\nstill matters even though no event travels through it directly.\n\n`userSelect`, `touchAction` and `enableContextMenu` are accepted and ignored:\nthey are Web-only upstream (no text selection to suppress, no CSS\n`touch-action`, no context-menu default to cancel on this platform), and\naccepting them keeps source that targets several platforms portable.\n\nA native ancestor further up the widget tree that steals the interaction\nmid-drag — a `ScrollView` above a `GestureDetector`, for instance reports as\na cancellation to every recognizer built on it: `onEnd`/`onFinalize` fire with\n`success: false`, not a clean ending. The responder system tells a theft\n(GTK denies the claim) apart from an ordinary release by watching for the\n`->DENIED` transition on the GTK sequence and routing it to the cancel path\nrather than the release path.",
927
921
  },
928
922
  {
929
- doc: "docs/api.md",
930
- heading: "The boundary, and who drew it",
931
- text: "What is implemented and what refuses is decided by **upstream's own\nnon-native build** the `.ts` files it ships next to its `.native.ts` ones,\nwhich are what react-native-windows and the web run. Where that build\ncomputes something, so does this; where it throws, this refuses by name. That\nis the only boundary here with a source of truth, and it draws itself in the\nright place: a worklet runtime is a **second JS runtime**, and this platform\nhas one thread. Measured against `react-native-worklets` 0.11.3.\n\nOne thing that build does is deliberately **not** copied, and it is a timing\nrather than a boundary: its UI hop waits for a `requestAnimationFrame`. That\nis the web standing in for a UI runtime it has not got, and React Native — the\ncontract here posts to a real thread that picks the job up without waiting\nfor a frame. See `docs/research/dnd-hover-flicker.md` for what the wait broke.\n\n| Export | Behaviour |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `runOnUI`, `scheduleOnUI`, `runOnJS`, `scheduleOnRN` | Deferred, not inlined, and returning `void` — the same functions `react-native-gtkx/reanimated` exports, so see that section's differences table. |\n| `runOnUIAsync` | Resolves with the worklet's return value when the UI hop runs it. The one thread API that hands anything back, because a promise crosses the deferral the others impose. |\n| `isWorkletFunction` | Upstream's `__workletHash` check. This platform never runs the Babel plugin, so nothing is a worklet and nothing needs to be — `'worklet'` is an inert string. |\n| `makeShareableCloneRecursive`, `createSerializable`, `makeShareable`, `makeShareableCloneOnUIRecursive`, `isSerializableRef`, `isShareableRef` | Identity, as upstream's own non-native serializer: a value never leaves the runtime it was made in, so there is nothing to clone. |\n| `serializableMappingCache`, `shareableMappingCache`, `registerCustomSerializable`, `callMicrotasks` | No-ops, as upstream. |\n| `isShareable`, `isSynchronizable` | Upstream's structural checks, ported unchanged. |\n| `RuntimeKind`, `getRuntimeKind`, `isRNRuntime`, `isUIRuntime`, `isWorkerRuntime`, `isWorkletRuntime`, `UIRuntimeId` | Answer for the one runtime there is: `ReactNative`. Upstream's non-native path reports the same, because its initializer sets that kind and nothing ever changes it. |\n| `getStaticFeatureFlag`, `getDynamicFeatureFlag`, `setDynamicFeatureFlag`, `isBundleModeEnabled`, `toggleSlowAnimationsOnUIRuntime` | `false` and no-ops. These gate upstream's native experiments and its Babel bundle mode, none of which exist here. |\n| `createWorkletRuntime`, `runOnRuntime`, `runOnRuntimeSync`/`Async`(`WithId`), `scheduleOnRuntime`(`WithId`), `getUIRuntimeHolder`, `getUISchedulerHolder` | **Throw**, naming themselves. A second runtime is structural, and upstream's own `runtimes.ts` throws for every one of these on any single-runtime build. |\n| `runOnUISync`, `executeOnUIRuntimeSync` | **Throw.** Both are \"run it over there and give me the answer now\". Deferring instead would be worse than refusing: the caller wants the return value, and a deferred call has none. |\n| `createShareable`, `createSynchronizable` | **Throw** memory shared between runtimes. |\n| `WorkletsModule` | **Throws**, naming itself. The one deliberate deviation from the mirror rule: upstream's non-native build exports it as `null`, which fails naming nothing. |\n\nA symbol not listed at all fails earlier still, at bundle time, with the\nbundler's own \"no export named X\".",
923
+ doc: "docs/reference/gesture-handler.md",
924
+ heading: "Recognizers",
925
+ text: "All ten recognizers run on one shared state machine `UNDETERMINED` →\n`BEGAN` (`ACTIVE` `END`) or `FAILED`/`CANCELLED` with the difference\nbetween kinds being which predicates the machine evaluates and which\ncallbacks are offered. `Tap` and `LongPress`, for example, are the same\nmachine as `Pan` with different predicates over the same event stream and the\nsame grant channel.\n\nEvery recognizer has two spellings: the chainable builder (`Gesture.Pan()`,\ndeprecated upstream since 3.1.0 but still what most shipped consumers call)\nand a hook (`usePanGesture()`, the spelling upstream is migrating to). Both\nproduce the same internal gesture spec; neither is a second implementation.\n\n| Recognizer | Builder | Hook | Input it needs | Reports travel (`onUpdate`/`onChange`) |\n| ---------- | ---------------------- | ---------------------------- | ------------------- | -------------------------------------- |\n| Pan | `Gesture.Pan()` | `usePanGesture()` | pointer | yes |\n| Tap | `Gesture.Tap()` | `useTapGesture()` | pointer | no |\n| LongPress | `Gesture.LongPress()` | `useLongPressGesture()` | pointer | no |\n| Native | `Gesture.Native()` | `useNativeGesture()` | pointer | yes |\n| Pinch | `Gesture.Pinch()` | `usePinchGesture()` | touchpad | yes |\n| Rotation | `Gesture.Rotation()` | `useRotationGesture()` | touchpad | yes |\n| Fling | `Gesture.Fling()` | `useFlingGesture()` | pointer | no |\n| Manual | `Gesture.Manual()` | `useManualGesture()` | pointer | yes |\n| Hover | `Gesture.Hover()` | `useHoverGesture()` | pointer (no button) | yes |\n| ForceTouch | `Gesture.ForceTouch()` | (upstream has none either) | stylus | yes |\n\nPinch and Rotation are driven by a touchpad rather than by the pointer, and\nForceTouch is driven by a stylussee\n[the recognizers that need other hardware](#pinch-and-rotation--the-two-that-need-a-touchpad)\nbelow. Every other kind runs on the ordinary pointer stream.",
932
926
  },
933
927
  {
934
- doc: "docs/platform-layer.md",
935
- heading: "Why you would reach for it",
936
- text: "- A GTK capability that React Native has no concept of: a real\n `Adw.NavigationView` stack, a `GtkListBox` row, a native `GtkEntry`.\n- Your own router, or no router: drive navigation from `useState`, a reducer,\n a URL, a state machine.\n- A property we did not think to surface in the navigator's options. Every\n widget below is re-exported straight from the gtkx bindings, so the full\n GObject property and signal surface is yours including properties added\n to gtkx after this page was written.",
928
+ doc: "docs/reference/gesture-handler.md",
929
+ heading: "One pointer, not multiple touches",
930
+ text: "There is exactly one pointer on this platform, and every payload's\n`pointerType` reads `MOUSE` except on `ForceTouch`, which reads `STYLUS` the\nonly kind whose reading is honestly not a mouse. The responder system\nfabricates one touch per pointer and has no virtual-touch protocol to draw a\nsecond contact point from. `minPointers(2)`, `numberOfPointers(2)`, and every\nother multi-pointer configuration are therefore honestly unreachable: those\nrecognizers simply never activate, rather than silently behaving as if a\nsingle finger satisfied a two-finger requirement.",
937
931
  },
938
932
  {
939
- doc: "docs/platform-layer.md",
940
- heading: "Declarative primitives",
941
- text: "These are the two components we wrap, because a raw `Adw.NavigationView` is\nimperative (`push`, `pop`, `pop_to_tag`) and React is not.\n\n| Export | What it is |\n| --------------------- | ------------------------------------------------------ |\n| `NavigationStack` | `Adw.NavigationView` driven by a `stack` array of tags |\n| `NavigationStackPage` | one page of that stack, identified by `tag` |\n\nThey **inherit every prop of the underlying widget** and only add to it, so\nanything you could set on `Adw.NavigationPage` you can set on\n`NavigationStackPage`.",
933
+ doc: "docs/reference/gesture-handler.md",
934
+ heading: "Common configuration and callbacks",
935
+ text: "Every recognizer accepts:\n\n| Option | Effect |\n| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `enabled` | Turns the recognizer on or off. |\n| `hitSlop` | Extra area a press still counts in, in RNGH's spelling — a plain number, or a per-edge object. Unlike a `View`'s own `hitSlop`, a negative number **shrinks** the area, and `{ left: 0, width: 32 }` anchors a strip to one edge. |\n| `shouldCancelWhenOutside` | Whether wandering off the view cancels the gesture. Defaults differ per kind — noted in each section below. |\n| `manualActivation` | Only an explicit `GestureStateManager`/`.activate()` call can activate the gesture; the ordinary predicate is not enough on its own. |\n| `withRef()` / a raw handler tag | Names this gesture for a relation written on another one. |\n| `withTestId()` / `testID` | A label carried on the config for introspection. |\n\nAnd the callbacks:\n\n| Callback | Fires |\n| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- |\n| `onBegin` | Entering `BEGAN`. |\n| `onStart` (`onActivate` in the hook spelling) | Entering `ACTIVE`. |\n| `onUpdate`, `onChange` | On travel — continuous kinds only; see the per-kind sections. |\n| `onEnd` (`onDeactivate`) | Leaving `ACTIVE`/`BEGAN` for `END` or a cancellation. |\n| `onFinalize` | Always last, whatever the outcome. |\n| `onTouchesDown`, `onTouchesMove`, `onTouchesUp`, `onTouchesCancelled` (`onTouchesCancel`) | Raw touch data, independent of the recognizer's own state. |\n\nThe builder spelling's ending callbacks take `(event, success)`; the hook\nspelling instead reads a `canceled` field off one event argument, and has no\n`onChange` at all `changeX`/`changeY` are always present on the update\npayload. `Tap`, `LongPress` and `Fling` are discrete and offer no\n`onUpdate`/`onChange` in either spelling: a gesture with no travel to report\nhas nothing for those callbacks to carry.\n\n`runOnJS` is accepted and does nothing: it asks for the JS runtime, and there\nis exactly one runtime here, so every callback already runs where it is\nasking. `averageTouches`, `enableTrackpadTwoFingerGesture`,\n`cancelsTouchesInView`, `activeCursor` and `mouseButton` are accepted and\ninert — each is platform-specific upstream too (Android-only, iOS-only or\nWeb-only respectively), and inert off its own platform there as well.\n\nThe three relation methods — `simultaneousWithExternalGesture`,\n`requireExternalGestureToFail`, `blocksExternalGesture` — are covered in\n[Cross-gesture relations](#cross-gesture-relations).",
942
936
  },
943
937
  {
944
- doc: "docs/platform-layer.md",
945
- heading: "React Native content inside GTK slots",
946
- text: "| Export | Sizing | Use for |\n| ------------------ | ---------------------------- | ------------------------------------------------ |\n| `SlotContent` | fills the slot | a page body, a pane, a dialog body |\n| `IntrinsicContent` | sized by its own Yoga layout | an AdwHeaderBar slot, a toolbar area, a list row |\n\n`createSidebarNavigator`'s `sidebarRow` screen option (docs/api.md) wraps\nits content in exactly `IntrinsicContent` for this reason a row is sized\nby what it holds, not stretched to fill the list.\n\n**Every content area inside a widget needs one of them.** A widget hands out\nrectangles two ways: as ordinary CHILDREN (a content area) and as SLOTS —\nproperties that take a widget, `topBar={…}`, `titleWidget={…}`, `sheet={…}`.\nWhich way a given area arrives is gtkx's business and moves between releases\n(rc.3 took the `content`/`child` props off single-child widgets and made that\ncontent a child), and it has never had anything to do with layout. Both are\nGTK's territory: the layout root is cleared on the way in, so a widget lands\nbare (what `WidgetContent` does by hand) and React Native content has to bring\nits own root.\n\n```tsx\n<AdwBottomSheet\n style={{ flex: 1 }}\n sheet={\n <IntrinsicContent>\n <View style={{ padding: 20, gap: 10 }}>…</View>\n </IntrinsicContent>\n }\n bottomBar={\n <IntrinsicContent>\n <View style={{ flexDirection: \"row\", gap: 8 }}>…</View>\n </IntrinsicContent>\n }\n>\n {/* the content area — a child under rc.3, and just as much a boundary */}\n <SlotContent>\n <View style={{ flex: 1, justifyContent: \"center\" }}>…</View>\n </SlotContent>\n</AdwBottomSheet>\n```\n\nForget the wrapper and you get an error naming the widget and where the\ncontent landed, not a wrong-looking window: without a root, content inside a\nwidget would join the ENCLOSING Yoga tree — laid out against the window's\nviewport while GTK hands it the widget's own rectangle.\nThe gallery's \"Widget hosting\" section is that whole story in one screen.\n\nWhich of the two is yours to choose, and the platform deliberately does not\nguess: `AdwBottomSheet` alone FILLS in its content child but HUGS in both\n`sheet` and `bottomBar`. One widget, three content areas, two answers, with\nnothing in the name or the GIR type to tell them apart — the answer lives in\nthe widget's own layout code. Swapping them is visible immediately:\n`SlotContent` in a bottom bar collapses it to nothing (a filling root reports\na zero minimum, so a size-to-content area is told \"nothing\"), and\n`IntrinsicContent` around a content area leaves `flex: 1` with no viewport to\nfill.\n\nNote the two independent sizes here. `style={{ flex: 1 }}` on the widget is\nthe WIDGET's size in the surrounding React Native layout (a wrapped widget is\na Yoga leaf at its natural size until the style says otherwise); the wrapper\ninside each content area is the CONTENT's size inside the rectangle that\nwidget then hands out.",
938
+ doc: "docs/reference/gesture-handler.md",
939
+ heading: "Pan",
940
+ text: "| Option | Effect |\n| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `activeOffsetX` / `activeOffsetY` / `failOffsetX` / `failOffsetY` | A single number is directional by its sign `activeOffsetX(20)` bounds only the positive side. Failure is tested with strict comparisons where activation uses non-strict ones, so a translation exactly on a bound activates. |\n| `minDistance` | Defaults to 10, unless an `activeOffset*` or `minVelocity*` option is set then distance stops applying and those are the criteria instead. |\n| `minVelocity`, `minVelocityX`, `minVelocityY` | Velocity thresholds, in addition to or instead of distance. |\n| `minPointers`, `maxPointers` | Pointer-count bounds see [One pointer, not multiple touches](#one-pointer-not-multiple-touches). |\n| `activateAfterLongPress` | Activates on a timer rather than on the next pointer movement. `0` (the default) means no hold at all. |\n\n`translationX`/`translationY` are measured from the point of activation, not\nfrom the press — a fresh grab always starts at zero, which is why an app\ncapturing a running offset does so in `onStart` rather than by reading the\ntranslation directly (see the example at the top of this page).\n`velocityX`/`velocityY` are the last inter-event delta, not a smoothed\nfigure see [the fling deviation](#fling) below, which reads the same\nnumber.",
947
941
  },
948
942
  {
949
- doc: "docs/platform-layer.md",
950
- heading: "Adwaita chrome, written in React Native",
951
- text: "The opposite direction from everything above: these create no widget an app\ncould not have created itself, because what they encode is a **look**, not a\nbehaviour.\n\n| Export | What it is |\n| ------ | -------------------------------------------- |\n| `Icon` | a **named** icon from the desktop icon theme |\n\n`Icon` is not `Image`: RN's `Image` takes a file path or URI, because on iOS\nand Android an icon is a bundled asset. Here it is a _name_ resolved against\nthe current icon theme at paint time, which recolours itself with the label\ncolour and follows the user's theme nothing in `Image`'s contract can\nexpress that. The shape is the one RN apps already use\n(`<Icon name size />`), with the desktop icon theme behind it instead of a\nbundled font.\n\n#### `List`/`ListRow`/`ListSeparator` were here, and are not any more\n\nThey were Adwaita's `.boxed-list` re-implemented in React Native the frame,\nthe separators, the corner radii, both tints plus an id-keyed\ndrag-to-reorder. Both halves are gone, for two separate reasons, and the\nreasoning is here rather than in a changelog because it is the general rule\nfor what belongs in this subpath.\n\n**The components.** The argument for shipping them was that a screen shared\nwith iOS and Android could not import `react-native-gtkx/adw`. That argument\ndoes not survive contact with the resolver: **`react-native-gtkx/common` does\nnot resolve on iOS or Android either.** Either import needs a `.linux.tsx`\nsplit or a `Platform` check, so `List` bought a shared screen nothing over\n`AdwActionRow` while costing a hand-maintained copy of libadwaita's own\nmetrics that drifts every time libadwaita moves. And the widget is better\nwhere it works: GTK's real keynav, focus and accessibility, with the numbers\ncoming from the system theme instead of from our source. They had exactly one\nconsumer in this repo.\n\nSo:\n\n- **want a native list** → `AdwActionRow`, `AdwEntryRow` and friends from\n [`react-native-gtkx/adw`](api.md), in a `GtkListBox` with\n `cssClasses={[\"boxed-list\"]}` see the gallery's \"Adwaita stack\" section;\n- **want that look written in React Native** copy\n [`examples/tasks-nav/src/components/list.tsx`](../examples/tasks-nav/src/components/list.tsx).\n It is 200 lines of `View`, `Pressable`, `Text` and `StyleSheet` with the\n measurements in comments, and it is meant to be copied.\n\nWhat survives from the change that introduced them (#47) is the part that\nmattered, and it is still platform surface: **`boxShadow`, `outline*` and\n`textDecorationLine` in the style layer**. Those are what make an\nAdwaita-looking list expressible in `StyleSheet` at all — the frame is a\nthree-part `box-shadow` rather than a border, and the focus ring is an\n`outline`, which takes no layout space. The finding was the style props, not\nthe components built on them.\n\n**The reorder.** `onReorder`/`reorderId` was a second, id-keyed entry point\ninto the same module `Draggable` and `Sortable` come from — two ways to drag,\none of them shaped like nothing an RN developer had seen. There is now one:\n[`react-native-gtkx/dnd`](api.md#drag-and-drop-react-native-gtkxdnd), which\nmirrors `react-native-reanimated-dnd`. An id-keyed reorder is a `Droppable`\naround a `Draggable` per row inside one `DropProvider`;\n`examples/tasks-nav/src/components/task-row.tsx` is that, and says in a\ncomment what it costs against the two lines it replaced.",
943
+ doc: "docs/reference/gesture-handler.md",
944
+ heading: "Tap",
945
+ text: "`Tap` activates on the **release**, not on the press, so the interaction stays\navailable to anything else watching the same pointer while a tap is still\nbeing decided it never holds the responder until the instant it wins.\n\n| Option | Effect |\n| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `numberOfTaps` | Taps required. Between them the gesture stays `BEGAN`, holding nothing; `onBegin` fires once for the whole sequence. |\n| `maxDuration` | Defaults to 500ms, re-armed on every press of a sequence. A press held past it fails on the timer, pointer still down. |\n| `maxDelay` | Defaults to 500ms how long the next tap may take to arrive before the sequence gives up. |\n| `maxDistance` | A radius from the press, not a per-axis limitthe tap-vs-drag rule. **Has no default**, matching upstream: an unconfigured tap accepts any travel that stays inside the view. |\n| `maxDeltaX`, `maxDeltaY` | Per-axis limits, independent of `maxDistance`. |\n| `minPointers` | Checked against the most pointers the interaction ever had at once. Above 1, see [One pointer, not multiple touches](#one-pointer-not-multiple-touches). |\n| `shouldCancelWhenOutside` | On by default, from the constructor. A press that wanders off the view is not a tap on it. |\n\nDiffers from `react-native-gesture-handler`: `useTapGesture()` defaults\n`shouldCancelWhenOutside` to `true` here, matching `Gesture.Tap()`. Upstream's\nown hook forgets to set this default even though its builder and its native\nhandler config both do, so its two spellings disagree with each other; both\nspellings agree here.",
952
946
  },
953
947
  {
954
- doc: "docs/platform-layer.md",
955
- heading: "GTK widgets, driven by React Native",
956
- text: 'Every `GtkWidget` subclass gtkx binds 86 of them at last count, from\n`GtkBox` and `GtkButton` to `GtkColumnView` and `GtkEmojiChooser`. The list is\ngenerated, not hand-picked: `scripts/generate-widget-surface.ts` classifies\ngtkx\'s full binding by real GObject inheritance (see\n`scripts/widget-surface/classification.json` for the exact list gtkx binds\ntoday) and `src/gtk/widgets.generated.ts` is the committed result. Re-run the\ngenerator after a gtkx upgrade to pick up new widgets it diffs against its\nown previous output and prints what changed.\n\nThey keep **every prop gtkx binds** and gain `style` and `onLayout`. Position\nand appearance both come from the style prop, exactly like anywhere else in\nReact Native:\n\n```tsx\n<View style={{ flexDirection: "row", gap: 8, alignItems: "center" }}>\n <GtkEntry\n style={{ flex: 1 }}\n placeholderText="Filter"\n />\n <GtkButton\n style={{ width: 72, backgroundColor: "#3584e4", borderRadius: 6 }}\n label="Go"\n />\n</View>\n```\n\nThe entry flexes, the button takes its width and its colour. The layout half\nof the style drives Yoga; the visual half becomes a GTK CSS class **on the\nwidget itself**, so the button really is blue, not a blue box behind a button.\nSet no size and the widget\'s own natural size wins.\n\n**Outside React Native layout they step aside.** The same `GtkButton` dropped\ninto a `AdwHeaderBar`\'s `start` or a `AdwToolbarView`\'s `topBar` — where there is no\nYoga tree to join renders as the bare widget. One symbol, both worlds, no\nflag to remember.',
948
+ doc: "docs/reference/gesture-handler.md",
949
+ heading: "LongPress",
950
+ text: "`LongPress` activates on a **timer**, with the pointer standing still —\nwaiting for the next pointer movement would mean waiting forever for a\npress-and-hold.\n\n| Option | Effect |\n| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `minDuration` | Defaults to 500ms. |\n| `maxDistance` | Defaults to 10, measured from the press for the whole gesture rather than re-based at activation. Travelling past it before the press matures fails the gesture; travelling past it after cancels it, so `onEnd`/`onFinalize` report `false`. |\n| `numberOfPointers` | Above 1, see [One pointer, not multiple touches](#one-pointer-not-multiple-touches). |\n| `shouldCancelWhenOutside` | On by default. |\n| `event.duration` | Milliseconds since the press. Upstream carries this on `LongPress` alone; every payload here carries it, since there is one payload type across all ten kinds. |\n\nDiffers from `react-native-gesture-handler`: `minDuration(0)` activates on the\nnext tick rather than synchronously inside the press. Nothing observable\ndepends on the difference.",
957
951
  },
958
952
  {
959
- doc: "docs/platform-layer.md",
960
- heading: "Unwrapped by necessity",
961
- text: "Two families of widget are exported **raw** instead of wrapped, because a\nwrapper box around them would be invalid GTK rather than a convenience:\n\n- **toplevels** — everything that implements `GtkRoot`: `GtkWindow` and\n everything that derives it (every `Gtk*Dialog`, `GtkApplicationWindow`,\n `GtkAssistant`, `GtkShortcutsWindow`, and their Adwaita counterparts\n `AdwWindow`, `AdwApplicationWindow`, `AdwAboutWindow`, `AdwMessageDialog`,\n `AdwPreferencesWindow`) plus `GtkDragIcon`, which derives `Gtk.Widget`\n directly and is a toplevel all the same. A wrapper box around a window is\n not a layout, it is two windows; and a drag icon inside one is a widget\n GTK cannot present at all. The rule is written against `GtkRoot` (the\n capability: owns its own surface, is presented, never parented) rather\n than against `Gtk.Window` (one familiar instance of it) precisely because\n `GtkDragIcon` is the case a class-shaped rule misses. `GtkPopover` sits on\n the other side of the line — a `GtkNative` but not a `GtkRoot`, and gtkx\n parents it with `gtk_popover_set_parent`, so it stays wrapped. Build a\n drag icon the way GTK does, from the drag itself\n (`Gtk.DragIcon.getForDrag(drag).setChild(…)`), not by mounting one.\n- **child-only widgets** valid solely as the direct child of one specific\n parent. `GtkListBoxRow` and `GtkFlowBoxChild` (plus everything that derives\n them every Adwaita preferences row, `AdwActionRow` included) are caught\n mechanically, by real inheritance. `AdwNavigationPage` and\n `AdwPreferencesPage` derive `Gtk.Widget` directly with no shared base to\n catch them mechanically, so they are a two-entry, doc-verified denylist\n instead see `scripts/widget-surface/classify.ts` for the exact reasoning\n behind each.\n\n`GtkGestureClick` is a third, simpler case: an event controller, not a\nwidget at all, so it was never a candidate for wrapping in the first place.\n\nNothing here is unreachable every raw export above is still exported,\nby name, from `react-native-gtkx/gtk` or `/adw`, exactly as gtkx binds it.",
953
+ doc: "docs/reference/gesture-handler.md",
954
+ heading: "Native",
955
+ text: '`Native` stands for the widget **underneath** the detector the one\nplatform-specific rule that follows from that is that it never takes the\nresponder. Taking it is what makes this platform claim `CLAIMED` on the GTK\nsequence and suspend kinetic scrolling on every enclosing scrollable, and a\ngesture whose whole meaning is "the native scroller is handling this" cannot\nbe the thing that switches the native scroller off. It reports what happens\nand yields.\n\n| Option | Effect |\n| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| activation | `BEGAN` on press, `ACTIVE` once the pointer has traveled 15pxwhere a native scrollable would have started scrolling. A lift before that fails rather than ends. |\n| `shouldActivateOnStart` | Takes the gesture on the press itself the shape for a native view that is a button rather than a scrollable. |\n| `disallowInterruption`, `yieldsToContinuousGestures` | Recorded on the config, for the relation registry to read; neither changes behavior by itself. |\n| `shouldCancelWhenOutside` | On by default. |\n| the callbacks | All present; `Native` is continuous, so it reports `onUpdate`/`onChange` travel like `Pan`. They arrive from the touch props (which fire regardless of responder status) rather than from the responder move event, since `Native` never holds the responder. |\n| a sequence taken away mid-drag | Reported as a cancellation see [the ancestor-steals-the-sequence note](#gesturedetector) above. |',
962
956
  },
963
957
  {
964
- doc: "docs/platform-layer.md",
965
- heading: "Auxiliary objects, not widgets at all",
966
- text: 'A further set of real JSX elements gtkx provides are not `Gtk.Widget` or\n`Adw.Widget` subclasses either, so `scripts/generate-widget-surface.ts`\nnever sees them at all same reason `GtkGestureClick` above is hand-kept\nrather than generated, just a wider set: actions and menus (`GSimpleAction`,\n`GMenu`), a responsive breakpoint (`AdwBreakpoint`), one option of an\n`AdwToggleGroup` (`AdwToggle` a segmented-control entry, not a widget of\nits own) and the two leaf elements an `AdwShortcutsDialog` is built from\n(`AdwShortcutsSection`, `AdwShortcutsItem`), a text buffer and an\nadjustment the model objects `GtkTextView`/spin- and scale-style widgets\nbind to (`GtkTextBuffer`, `GtkAdjustment`), keyboard shortcuts\n(`GtkShortcut`, `GtkShortcutController`), and the two drag-and-drop\ncontrollers (`GtkDragSource`, `GtkDropTarget`). All of them are exported, by\nname, from `react-native-gtkx/gtk` or `/adw`, next to `GtkApplication` and\n`GtkGestureClick`.\n\n```tsx\n<GtkApplicationWindow\n actions={\n <GSimpleAction\n name="new"\n onActivate={onNew}\n />\n }\n breakpoints={\n <AdwBreakpoint\n condition={Adw.BreakpointCondition.parse("max-width: 500sp")}\n onApply={() => setCollapsed(true)}\n onUnapply={() => setCollapsed(false)}\n />\n }\n/>\n```\n\n**One caveat found while building `examples/tasks-app`, worth knowing before\nyou rely on it in a test:** `AdwBreakpoint`\'s `onApply`/`onUnapply` never\nfired in the `@gtkx/vitest` headless-sway gtk test project, even with a\ngenuine `swaymsg` resize past the condition\'s threshold (see\n`packages/react-native-gtkx/tests/gtk/bridge/auxiliary-elements.gtk.test.tsx`)\n but it works exactly as documented in a real GNOME session (verified with\na throwaway app launched via `node scripts/vm.ts app`). Treat it as untestable\nunder headless sway today, not as broken.',
958
+ doc: "docs/reference/gesture-handler.md",
959
+ heading: "Pinch and Rotation the two that need a touchpad",
960
+ text: "![The gallery's Pinch and rotation section: Gesture.Pinch() and Gesture.Rotation(), both driven by a touchpad.](../shots/gallery/gesture-pinch.png)\n\nBoth are driven by a touchpad rather than by the pointer: a pinch is not a\npointer event, it is a conclusion libinput draws from two fingers moving on a\ndevice it has classified as a touchpad, delivered as\n`zwp_pointer_gestures_v1` and turned by GDK into `GDK_TOUCHPAD_PINCH`. `GtkGestureZoom`\nand `GtkGestureRotate` read the scale and the angle directly off that event\nrather than reconstructing them from tracked positions a more direct path\nthan upstream's own `ScaleGestureDetector`, which tracks two real touches and\nhas no touchpad path of its own. With no touchpad attached, neither gesture\never begins; a mouse cannot produce the input either recognizer needs.\n\nRecognition and arbitration are otherwise unchanged: the same state machine,\nthe same callbacks, the same relation maps, the same broadcast cancel as\nevery other kind. `Gesture.Simultaneous(pinch, rotation)` behaves exactly like\n`Gesture.Simultaneous(pan, tap)`; without a relation, a `Pinch` and a\n`Rotation` race and cancel each other like any other two gestures would.\n\n```tsx\nconst scale = useSharedValue(1)\nconst angle = useSharedValue(0)\n\nconst pinch = Gesture.Pinch().onUpdate((event) => {\n scale.value = event.scale // 1 at the start, cumulative, >1 for a spread\n})\nconst rotation = Gesture.Rotation().onUpdate((event) => {\n angle.value = event.rotation // radians since the start, positive clockwise\n})\n\nconst zoomable = (\n <GestureDetector gesture={Gesture.Simultaneous(pinch, rotation)}>\n <Animated.View style={animatedStyle} />\n </GestureDetector>\n)\n```\n\nNeither recognizer has any configuration of its own beyond what every kind\nshares — matching upstream, where `PinchGesture` and `RotationGesture` add\nzero builder methods over their common base.\n\n| Field | Value |\n| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |\n| `event.scale` | Cumulative and multiplicative, 1 at the start of the gesture, and not re-based when it activates. |\n| `event.scaleChange` | A ratio (scale composes by multiplication) — the `scale` itself on the first update. |\n| `event.rotation` | Radians since the start of the gesture, positive clockwise. |\n| `event.rotationChange` | A difference in radians. |\n| `event.focalX`/`focalY`, `event.anchorX`/`anchorY` | In the gesture view's own coordinates; `absoluteX`/`absoluteY` carry the same point in window coordinates. |\n| `event.velocity` | Per second — scale-per-second for `Pinch`, radians-per-second for `Rotation`. See the deviation note below. |\n| activation | `Rotation` at of accumulated rotation (upstream's own threshold). `Pinch` at 5% of accumulated scale change. |\n| `shouldCancelWhenOutside` | Off by default — a pinch is not addressed to a point the way a tap is, so a focal point drifting off the view mid-gesture does not cancel it. |\n| the `onTouches*` callbacks | Accepted, and never fire — there is no touch sequence behind a touchpad gesture, matching upstream's own behavior on a trackpad. |\n| pinch-specific / rotation-specific config | None, upstream included. |\n\nDiffers from `react-native-gesture-handler`, in two places, both named\nexplicitly rather than silently reproduced:\n\n- **Velocity units.** `event.velocity` is computed per second here, which is\n what upstream's own documentation promises but not what either of its web\n handlers actually computes: `PinchGestureHandler` divides by a millisecond\n delta and never by 1000 (a thousand times too small), and\n `RotationGestureDetector`'s time delta is an addition of two timestamps\n rather than a subtraction, which is not a velocity at all. There is no\n single correct upstream number to reproduce, so the documented unit is what\n ships.\n- **Pinch's activation threshold.** `Pinch` activates at 5% of accumulated\n scale change. Upstream activates after two stages of pixel arithmetic — 30px\n of span change, then a further 15px — which has nothing to measure here: a\n touchpad pinch arrives as a ratio, with no pixel span anywhere in the chain.\n A percentage is the restatement, and a small one is the correct scale for\n it, because libinput has already decided the two fingers are pinching rather\n than scrolling before GTK ever sees the event — upstream's own threshold is\n the first such decision in its pipeline, this one is a second, smaller gate\n after that decision has already been made elsewhere.\n\nBoth gestures need a real touchpad and a compositor with a libinput backend to\nobserve; the headless compositor this project's own test suite runs against\nhas neither, so both are verified with a virtual touchpad device instead of\ninside that suite.",
967
961
  },
968
962
  {
969
- doc: "docs/platform-layer.md",
970
- heading: "The window and application AppRegistry built",
971
- text: '`useParentWindow` (the `Gtk.Window` ancestor), `useApplication` (the\n`Adw.Application` `.sendNotification(id, notification)` is the common\nreason to reach it) and `quit` (the same function `AppRegistry` wires to a\nwindow\'s own close button) are re-exported from `react-native-gtkx/gtk`.\nNone of these give you the window or application object ITSELF to build —\n`AppRegistry.runApplication` already did thatthey let already-mounted\ncode reach back into it, the same way `useBindSetting` needs a `Gtk.Window`\nto bind a `defaultWidth` property on:\n\n```tsx\nconst window = useParentWindow()\nuseBindSetting({\n schema,\n key: "window-width",\n object: window,\n property: "defaultWidth",\n})\n```\n\nFor the other half of reaching back into that window putting things ON it\nrather than reading it see the next section.',
963
+ doc: "docs/reference/gesture-handler.md",
964
+ heading: "Fling",
965
+ text: "The distinguishing fact about a fling is that it is a velocity predicate, not\na distance one — a slow drag can travel exactly as far as a fast flick.\n\n| Option / rule | Value |\n| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `direction` | A bitmask of `Directions`, defaulting to `Directions.RIGHT`. Setting two axis bits also opens the diagonal between them, with a wider cone — `UP \\| RIGHT` accepts a 45° flick that neither axis accepts alone. |\n| the cones | 30° around each axis (±15°), 60° around each diagonal (±30°) — tiling the circle exactly, matching upstream. |\n| `minVelocity` | 700 units per second, compared strictly. Not configurable, upstream included. |\n| the deadline | 800ms from the press. A press that has not flung by then fails, whatever it is doing. |\n| `numberOfPointers` | Compared for equality against the most pointers the interaction ever had see [One pointer, not multiple touches](#one-pointer-not-multiple-touches). |\n| when it decides | On every move, not on release — the instant the pointer is fast enough and pointed the right way, button still down. The release is only the last chance. |\n| the progression | `BEGAN` `ACTIVE` `END` in one synchronous step, with no `onUpdate` ever — a fling is discrete. |\n\nDiffers from `react-native-gesture-handler`: `velocityX`/`velocityY` are the\nlast inter-event delta, the same number `Pan().minVelocity()` reads, rather\nthan upstream's least-squares fit over up to 20 samples inside a 300ms\nhorizon. A fling here is more sensitive to a single long frame than\nupstream's smoothed figure; the deadline and the cone are unaffected.",
972
966
  },
973
967
  {
974
- doc: "docs/platform-layer.md",
975
- heading: "Actions and shortcuts declared in the app tree",
976
- text: '`WindowActions`, `ApplicationActions` and `WindowControllers` register their\nchildren on the window (or the application) **from wherever they are written\nin the app tree**. They render nothing where they sit they are portals in\nReact\'s own sense: the children stay part of the tree at that position, with\nthe context, state and effects they would have there, while the registration\nlands on the window.\n\n```tsx\nimport { GSimpleAction, WindowActions } from "react-native-gtkx/gtk"\n\nconst NewTaskAction = () => {\n const { addTask } = useStore() // an ordinary React context store\n return (\n <WindowActions>\n <GSimpleAction\n name="new"\n onActivate={() => addTask()}\n />\n </WindowActions>\n )\n}\n```\n\nThat is `win.new` what a HeaderBar button\'s `actionName`, a `GMenu` item\nand an `actionAccels` entry all target. `ApplicationActions` is the same\ncomponent against the application\'s action map (`app.*`), and the two are\nNOT interchangeable: a `Gio.Notification`\'s action button can only ever\nactivate an application action, and an application action outlives any one\nwindow. Which prefix you get is decided by which component you write.\n`WindowControllers` takes `Gtk.EventController` children — a\n`GtkShortcutController` with `scope={Gtk.ShortcutScope.GLOBAL}` is the whole\nreason it exists.\n\n**Reach for these, not for `runApplication`\'s `applicationActions` /\n`windowActions` / `windowControllers` options.** Those options build their\nchildren as props of the window `AppRegistry` creates, which makes them\nSIBLINGS of the app tree: no provider inside the app is above them, so an\naction declared there cannot read a React context — `examples/tasks-nav` had\nto rewrite its store as a module-level external store before Ctrl+N could\nsee any app state at all. The options still work and are not going away\nwithout notice, but they are deprecated, and everything they can express the\ncomponents can express better:\n\n- **context works**, because the declaration is a descendant of its provider;\n- **registration is dynamic** — the action is added when the declaring\n component mounts and removed when it unmounts, so one screen can own its\n own actions instead of the process owning all of them for its lifetime;\n- **it composes** — two unrelated subtrees each declare their own without\n meeting in a single options object.\n\n`actionAccels` is NOT deprecated and stays a `runApplication` option: it is a\nflat name→keys table with no children and nothing to read from context, and\nit is deliberately process-wide (an accelerator naming an action that is not\nregistered right now simply does nothing). A shortcut that should come and\ngo with a screen is a `GtkShortcutController` inside `<WindowControllers>`.\n\n**Two components, not one, on purpose.** Actions land on the window as a\n`Gio.ActionMap` (`addAction`/`removeAction`, keyed by NAME); controllers land\non it as a `Gtk.Widget` (`addController`/`removeController`, keyed by the\ncontroller object). Different children, different GObject interfaces, and —\nsee below — different duplicate semantics. One component sorting its children\nby type would fail silently on a wrong child; two fail at the type level.\n\n**A duplicated action name goes to the FIRST declaration**, and a second one\nis ignored with a development warning naming it. This is not a coin toss\nbetween first and last. `Gio.ActionMap` is name-keyed at both ends:\n`addAction` silently replaces a same-named action, and `removeAction` takes a\nname, not the action object. Under "last wins", the first of the two\ndeclarations to unmount would remove whatever currently answers to that\nname leaving the other one mounted but dead. First-wins is the only order\nin which release always precedes acquire: the loser never registers, and when\nthe winner unmounts (removing its own action, correctly) the claim passes to\nthe next declaration still mounted, which registers in a later commit. If you\nwant a screen to override a shortcut, give it its own name, or move the\ndeclaration somewhere both screens can reach.\n\n**Inside a `Modal`, the enclosing window is the modal\'s own window**, so\nactions and controllers declared there belong to it and go away with it —\nusually what a dialog wants, and worth knowing when it is not. Under\n`chrome: "content"` and inside the navigators nothing changes: the window is\nstill the one `AppRegistry` built, the navigators own widgets inside it and\nnot its action map, and a `HeaderBar` button in a page resolves `win.*`\nthrough the widget hierarchy up to that same window. One consequence worth\nknowing: react-navigation keeps a popped screen mounted until its exit\ntransition ends, so a screen\'s actions outlive the pop by the length of the\nanimation.',
968
+ doc: "docs/reference/gesture-handler.md",
969
+ heading: "Manual",
970
+ text: "No configuration of its own, in either spelling matching upstream, where\n`ManualGesture` adds zero builder methods. It begins on the press and decides\nnothing on its own: the `GestureStateManager` handed to\n`onTouchesDown`/`onTouchesMove`/`onTouchesUp`/`onTouchesCancel` is the whole\nAPI.\n\n| Method | Transition |\n| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `.begin()` | `UNDETERMINED` `BEGAN`. |\n| `.activate()` | `BEGAN` `ACTIVE`, through the ordinary arbitration a request, not a decision: it can come back parked behind `requireExternalGestureToFail`, or cancelled. Forced past `manualActivation`. |\n| `.end()` | `BEGAN` or `ACTIVE` `END`, successfully. |\n| `.fail()` | `BEGAN` or `ACTIVE` `FAILED`. |\n\nDiffers from `react-native-gesture-handler`: upstream's documentation states\nthat `Manual` does not end when the pointers lift. Half of that holds here —\na `Manual` still `BEGAN` when the pointer comes up stays `BEGAN`, holding\nnothing. The other half does not: an `ACTIVE` `Manual` here is holding an\ninteraction the responder lock, the GTK sequence, suspended scrollersand\nthat interaction ends when the pointer does. Staying `ACTIVE` past it would\nmean holding a lock that no longer exists and never reporting an ending at\nall, so an `ACTIVE` `Manual` ends, successfully, with the interaction.\n`onTouchesUp` fires first and carries the state manager, for an app that wants\na different ending to write it in.",
977
971
  },
978
972
  {
979
- doc: "docs/platform-layer.md",
980
- heading: "`Controllers` — a GTK event controller on a React Native component",
981
- text: "The same idea one level down. `<Controllers>` attaches its children to the\nwidget of the **enclosing React Native component** — `View`, `Pressable`,\n`ScrollView`, `Animated.View`, any of them:\n\n```tsx\n<Pressable onPress={open}>\n <Controllers>\n <GtkDragSource\n actions={Gdk.DragAction.MOVE}\n onPrepare={(x, y, self) =>\n Gdk.ContentProvider.newForValue(\n GObject.buildValue(GObject.TYPE_STRING, (v) => v.setString(id)),\n )\n }\n />\n </Controllers>\n <Text>{title}</Text>\n</Pressable>\n```\n\n**Why it exists.** A `Pressable`'s `ref` is a `ViewHandle` —\n`measure`/`measureInWindow`/`measureLayout` and that is correct: React\nNative's contract says nothing about widgets, and an app reaching through a\nref to a `Gtk.Widget` would pin every internal of this platform as public\nAPI. But GTK carries behaviour no style and no RN prop expresses,\ndrag-and-drop above all, and before this there was simply no way to reach it\nfrom a row written in React Native. `examples/tasks-nav`'s rows could not be\nrewritten because of it (see\n[the showcase research](research/react-native-first-showcase.md)).\n\n**Why a component here and not a `controllers` prop on `View`.** A prop\nwould have been three lines, and it would sit on the two components an app\nshares with iOS and Android, imported from the _portable_ entry point: the\nfile would still compile everywhere, the prop would be ignored off Linux,\nand the feature would vanish with no diagnostic. On this platform **the\nimport is the signal** — `react-native-gtkx/gtk` is the line an app knows it\nis crossing. An element is also something an RN developer already knows how\nto put behind a `Platform.OS` check or a `.linux.tsx` split, and its absence\nis visible in the tree.\n\nTwo properties follow from it being a portal, and both matter:\n\n- **it composes with context** the handler that reorders a list is written\n where that list's state already is;\n- **it is lifecycle-bound** attached on mount, removed on unmount, so a\n screen's controllers leave with the screen.\n\nOne caveat, stated rather than hidden: the controllers attach **one commit\nafter mount**. React attaches host refs bottom-up, so the enclosing view's\nwidget does not exist yet when a child's layout effects run. For an event\ncontroller this is unobservable — no pointer reaches a widget in its first\nframe — and it is the same delay `WindowActions` has. It does mean a test\nthat aims a synthetic pointer at a fresh tree has to let one commit land.\n\nInside a GTK widget's own slot there is no enclosing React Native component\nand nothing is attached; pass `controllers={…}` to the widget itself there,\nwhich is the prop this substitutes for.\n\nFor drag-and-drop specifically there is a whole module above this:\n[`react-native-gtkx/dnd`](api.md#drag-and-drop-react-native-gtkxdnd) mirrors\n`react-native-reanimated-dnd`'s API (`Draggable`, `Droppable`,\n`DropProvider`, `Sortable`) on these two controllers, and both bundler\npresets alias the package name onto it and `react-native-gesture-handler`\nonto a shim that keeps `GestureHandlerRootView` working — so a ported app\nkeeps its source unchanged.\nAll of it is written on top of `Controllers`, not around it which is the\nproperty that makes this subpath worth having.",
973
+ doc: "docs/reference/gesture-handler.md",
974
+ heading: "Hover",
975
+ text: "Driven by the same GTK motion controller `Pressable` uses for its `hovered`\nstate. It goes straight to `ACTIVE` on the pointer crossing in, with no\nthreshold at all, reports `x`/`y` in the gesture view's own coordinates while\nthe pointer moves inside, and ends not cancels when the pointer leaves.\n\n| Option | Effect |\n| --------------------------- | ------------------------------------------------------------------------------------------------------ |\n| `.effect()` / `hoverEffect` | Accepted, inert iOS's own pointer effect; upstream's web handler never branches on it either. |\n| `hitSlop`, `enabled` | As for every other kind, including the shrinking (negative) form of `hitSlop`. |\n| the callbacks | `Hover` is continuous, so `onUpdate`/`onChange` report travel and `changeX`/`changeY` carry the delta. |\n| `mouseButton` | Inert for this kind, matching upstream: hover never consults a button. |\n\nA hover never takes the responderthere is no press to start an interaction\nwith, so there is no session to claim. That means a hover cannot exclude a\npress by itself, and mutual exclusion is still the default: a hover crossing\nin while a `Pan` on another view is still `BEGAN` cancels that pan, matching\nupstream's own behavior. Declaring `simultaneousWithExternalGesture` (or\ncomposing with `Gesture.Simultaneous()`) between a hover and anything sharing\nits screen avoids that, the same way upstream's own `Pressable` sets\n`manualActivation` on its internal hover recognizer to stop it blocking a\nnative gesture.",
982
976
  },
983
977
  {
984
- doc: "docs/platform-layer.md",
985
- heading: "GSettings",
986
- text: '`useSetting` and `useBindSetting` come straight from `@gtkx/react`, re-\nexported from `react-native-gtkx/gtk` next to the `Gio` namespace they read\nand write through:\n\n```tsx\nconst [value, setValue] = useSetting(schema, "color-scheme")\nuseBindSetting({\n schema,\n key: "window-width",\n object: windowRef,\n property: "defaultWidth",\n})\n```\n\nTurning a `.gschema.xml` file into the `SettingsSchema` object these hooks\nexpect (`{ id, path, keys }`) is a build-time concern, not something this\nsubpath does `#data/your-schema.gschema.xml` resolves for free on the\n`gtkx dev`/`gtkx build` toolchain (the `gtkx:settings` vite plugin ships\ninside `@gtkx/cli` itself), the same way `examples/tasks-app` uses it. It is\nnot wired into the Metro toolchain (`react-native run-linux`) at all an\napp on that path has to construct the `SettingsSchema` object by hand\n(`{ id, path, keys: { "key-name": "s" } }`, matching the schema\'s own type\nstrings) or add its own build step.',
978
+ doc: "docs/reference/gesture-handler.md",
979
+ heading: "ForceTouch",
980
+ text: "Upstream does not implement `ForceTouch` off iOS at all, so there is no web\nbehavior to match — the semantics below come from its documented contract.\n\n| Option | Effect |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `minForce` | Defaults to 0.2, upstream's documented default. Non-strict at the bound, like every other activation threshold here. |\n| `maxForce` | A ceiling that fails the gesture before activation and cancels it after — the same shape `LongPress`'s `maxDistance` has. Unset means no ceiling. |\n| `feedbackOnActivation` | Accepted, inert there is no haptic device on this platform. |\n| `force`, `forceChange` | On every payload. `forceChange` is a difference (the force itself on the first update). |\n| `pointerType` | Always `STYLUS` for this kind — the only one that is not `MOUSE`, since a pressure reading can only have come from a tablet tool. |\n\n`ForceTouch` has no hook counterpart in either implementation — upstream's own\nhook tree has nine directories and no `forceTouch`, so `Gesture.ForceTouch()`\nis the whole API upstream offers for it, and the whole API offered here.\n\nIt is driven by `GtkGestureStylus`, whose pressure axis arrives already\nnormalized to `[0, 1]` upstream's documented range, so nothing is rescaled.\nThe controller is stylus-only by default, so **a mouse produces no events for\nit at all**: that is deliberate, and it is what keeps a `ForceTouch` from\nactivating at pressure 0 on a machine with no drawing tablet. Verifying the\nfull chain end to end needs a real or virtual stylus device; the headless\ncompositor this project's test suite runs against enumerates none.",
987
981
  },
988
982
  {
989
- doc: "docs/platform-layer.md",
990
- heading: "Adwaita structure",
991
- text: 'Every `Adw.Widget` subclass gtkx binds — 46 wrapped the same way as the GTK\nwidgets above, from `AdwAvatar` and `AdwCarousel` to `AdwToolbarView` and\n`AdwViewSwitcher`. `AdwHeaderBar` and `AdwToolbarView` now take `style` too,\nand still step aside into the bare widget in a slot that has no Yoga tree —\n`AdwToolbarView`\'s own `topBar` is exactly that kind of slot:\n\n```tsx\n<View style={{ flex: 1 }}>\n <AdwToolbarView\n style={{ flex: 1 }}\n topBar={<AdwHeaderBar showTitle={false} />}\n >\n <SlotContent>{/* … */}</SlotContent>\n </AdwToolbarView>\n</View>\n```\n\n`AdwNavigationView` and `AdwNavigationSplitView` are wrapped the same way;\n`NavigationStack` above is a declarative layer on top of the former, not a\nreplacement for it the raw widget is always one import away.\n\n`AdwApplicationWindow` (a toplevel) and `AdwNavigationPage` (valid only as a\ndirect child of `AdwNavigationView`/`AdwNavigationSplitView`) are exported\nraw see "Unwrapped by necessity" above.',
983
+ doc: "docs/reference/gesture-handler.md",
984
+ heading: "Gesture composition",
985
+ text: "`Gesture.Race()`, `Gesture.Simultaneous()` and `Gesture.Exclusive()` (and\ntheir hook equivalents `useCompetingGestures()`, `useSimultaneousGestures()`\nand `useExclusiveGestures()`) are list-builders over the three relation maps\ndescribed in [Cross-gesture relations](#cross-gesture-relations), with no\nmechanism of their own:\n\n- `Race` adds no relation at all racing is what happens without one.\n- `Simultaneous` is a pairwise fill of the simultaneous-handlers map.\n- `Exclusive` is a chain fill of the wait-for map, where every group waits for\n every group before it. A nested `Exclusive` inside a `Simultaneous` stays\n exclusive.\n\nA single `GestureDetector` may hold a composition. It mounts every recognizer\nthe composition contains onto the one child, and still adds no widget.",
992
986
  },
993
987
  {
994
- doc: "docs/platform-layer.md",
995
- heading: "Namespaces",
996
- text: "`Adw`, `Gdk`, `Gio`, `Gtk`, `Pango` — exported as values, because you need\nboth the runtime enums and the types:\n\n```tsx\n;<GtkScrolledWindow hscrollbarPolicy={Gtk.PolicyType.NEVER} />\nconst viewRef = useRef<Adw.NavigationView | null>(null)\n```",
988
+ doc: "docs/reference/gesture-handler.md",
989
+ heading: "Cross-gesture relations",
990
+ text: "![The gallery's Gesture relations section: Gesture.Native() over a ScrollView, simultaneousWithExternalGesture, and requireExternalGestureToFail.](../shots/gallery/gesture-relations.png)\n\n| Relation | Means |\n| -------------------------------------------------------------------------- | --------------------------------------- |\n| `requireExternalGestureToFail(other)` — hook config: `requireToFail` | This gesture waits for `other` to fail. |\n| `simultaneousWithExternalGesture(other)` — hook config: `simultaneousWith` | Both may be `ACTIVE` at once. |\n| `blocksExternalGesture(other)` hook config: `block` | `other` waits for **this** one. |\n\nA relation names the other gesture with the gesture object itself, a\n`withRef()` handle to it, or a raw handler tag. The gesture object built by\neither spelling is rebuilt on every render, so a relation should point at a\nmemoized object (`useMemo`, a ref, or a context value) — a relation written\nagainst a stale object of a gesture that has since been rebuilt cannot be\nresolved. Upstream has the same constraint.\n\n```tsx\nconst scroll = Gesture.Pan().activeOffsetX([-10, 10]).failOffsetY([-25, 25])\n\nconst sheet = Gesture.Pan()\n .activeOffsetY([-10, 10])\n // Held in BEGAN — taking nothing, claiming nothing — until `scroll` fails.\n .requireExternalGestureToFail(scroll)\n```\n\n**Two locks, at two levels, deliberately not merged.** The responder lock\nkeeps its one job: one interaction belongs to React Native, one holder, one\nirrevocable claim on the source. Gesture arbitration is a second, JS-only\nregistry that never talks to GTK — every relation resolves before anything is\nclaimed. The consequences:\n\n- `Simultaneous` really means two `ACTIVE` gestures, each getting its own\n `onStart`/`onUpdate`/`onEnd` for the same pointer — while exactly one\n responder is claimed. The gesture that did not win the responder lock is\n driven from the touch props, which fire regardless of responder status; the\n holder reads the responder-move event.\n- Mutual exclusion is the default. Without a relation, the first gesture to\n activate cancels every other gesture watching the same interaction. A\n gesture that is already `ACTIVE`, or parked waiting for another, is\n cancelled by nothing except an active `Gesture.Native()` — which is why\n `Native` is treated as special rather than as just another recognizer.\n- `END` and `FAILED` are not the same release for a parked gesture: one\n waiting on another is released when that one fails or is cancelled, and\n **cancelled** when it ends — the thing it was deferring to actually\n happened, so its own turn never comes.\n\nTwo responder roots that nest — an island mounted inside another island's\nview — are one GTK widget chain, so both gestures share one interaction path\nand every relation behaves as it would inside a single root. Two roots that\nare disjoint — separate windows, or sibling islands — can never have both\ngestures live in one interaction at once: a relation between them is\nexpressible and resolves to a real handler tag, it simply never has an\noccasion to apply, and it neither errors nor warns.\n`requireExternalGestureToFail` across disjoint roots does not deadlock for the\nsame reason: parking only ever happens against a gesture that is live in the\ninteraction under way, so a gesture in another root is never waited for.",
997
991
  },
998
992
  {
999
- doc: "docs/platform-layer.md",
1000
- heading: "Navigation without a router",
1001
- text: 'The stack is an array of tags. Change the array, the widget animates.\n\n```tsx\nimport { useState } from "react"\nimport { Pressable, Text, View } from "react-native"\nimport { AdwHeaderBar, AdwToolbarView } from "react-native-gtkx/adw"\nimport {\n NavigationStack,\n NavigationStackPage,\n SlotContent,\n} from "react-native-gtkx/common"\n\nconst App = () => {\n const [stack, setStack] = useState(["home"])\n\n return (\n <NavigationStack\n stack={stack}\n // The Adwaita back button, Escape, the back gesture and the\n // back-history menu all arrive here. Follow them in your own state.\n onPopped={(tag) => setStack((s) => s.filter((entry) => entry !== tag))}\n >\n <NavigationStackPage\n tag="home"\n title="Home"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <Pressable onPress={() => setStack((s) => [...s, "detail"])}>\n <Text>Open detail</Text>\n </Pressable>\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n\n <NavigationStackPage\n tag="detail"\n title="Detail"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <View />\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n </NavigationStack>\n )\n}\n```\n\nA runnable version is the gallery\'s "Adwaita stack" section three levels\ndeep, with React Native content in the header bar and a raw `GtkButton` beside\nit. Note that it wraps the `NavigationStack` in a `Widget`: the component\nrenders a RAW `Adw.NavigationView`, which has no Yoga node of its own, so\nnesting it inside a React Native layout root needs the measured leaf `Widget`\nprovides. As an app\'s root, where GTK allocates it directly, that is not\nneeded.',
993
+ doc: "docs/reference/gesture-handler.md",
994
+ heading: "GestureStateManager",
995
+ text: "`GestureStateManager.activate(handlerTag)`, `.fail(handlerTag)` and\n`.deactivate(handlerTag)` are standalone functions, keyed by a numeric handler\ntag rather than by a gesture object — the shape `react-native-gesture-handler`\n3.1.0 itself exports under this name (its older `.create(tag)` factory survives\nonly as a type, with no runtime value).\n\nEach call looks the tag up in a registry populated the instant a\n`GestureDetector` mints a handler tag for a mounted recognizer, and forgotten\nthe instant that detector unmounts, then routes to the same state-manager\nobject `Gesture.Manual()`'s own `onTouchesDown`/`onTouchesMove`/`onTouchesUp`/\n`onTouchesCancel` callbacks already receive the same machinery, the same\narbitration loop, nothing built twice.\n\n| Method | Effect |\n| ------------------------- | ------------------------------------------------------------------------------------------- |\n| `.activate(handlerTag)` | `BEGAN` `ACTIVE` on the recognizer that tag names, through the ordinary arbitration loop. |\n| `.fail(handlerTag)` | `BEGAN` or `ACTIVE` → `FAILED`. |\n| `.deactivate(handlerTag)` | `BEGAN` or `ACTIVE` → `END`, successfully — upstream's other name for the same transition. |\n| an unknown tag | A no-op, with a development-mode warning. |\n\nDiffers from `react-native-gesture-handler`: a tag naming no mounted\nrecognizer never minted, or already unmounted does not throw. It is a\nno-op, warned in development rather than in production, which is not\nupstream's own shape (a native lookup miss) but the closest match available:\nloud without being fatal, matching the same no-op a gesture's own state\nmachine already gives an out-of-order call.\n\n`react-native-sortables`' own v3 gesture-handler adapter calls\n`GestureStateManager.activate(event.handlerTag)` from its own\n`onTouchesMove`, reading only the numeric tag off the event — the ordinary\npath for any drag using that library, not an edge case.",
1002
996
  },
1003
997
  {
1004
- doc: "docs/platform-layer.md",
1005
- heading: "`NavigationStack` props",
1006
- text: "Everything `Adw.NavigationView` has, plus:\n\n| Prop | Meaning |\n| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stack` | ordered page tags, root first. This is the navigation state |\n| `animateTransitions` | forwarded straight to `Adw.NavigationView`'s own property. Default true GTK has one transition style, so this is only ever on/off |\n| `onPopped(tag)` | the WIDGET popped by itself. Not called for pops you caused by changing `stack` |\n| `onPageClosed(tag)` | a closing page finished animating out and left the tree |\n| `onTransitionStart()` / `onTransitionEnd()` | a push/pop/replace began / finished, the latter driven by the transitioning page's own `shown`/`hidden` signal |\n| `transitionDuration` | ms, default 400 a fallback window for retention and the callbacks above, used only when a page's own transition signal never arrives; not a measurement of the real transition length |\n| `ref` | the `Adw.NavigationView` itself, for anything not modelled here |\n\nPages not listed in `stack` are still accepted as children and simply are not\nshown, so a router may hand over all of its screens at once.\n\n**Exit animations are handled for you.** When a tag leaves `stack`, the widget\nstill animates the page out. `NavigationStack` keeps a snapshot of that page\nuntil its `hidden` signal (with a timer fallback for two cases where that\nsignal never arrives on its own: compositors that never emit it, and a page\nskipped over entirely by a multi-hop pop see `transitionDuration` above),\nso you never have to keep rendering pages you already consider gone.",
998
+ doc: "docs/reference/gesture-handler.md",
999
+ heading: "State, Directions and the other enums",
1000
+ text: "| Export | Values | Used for |\n| ------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `State` | `UNDETERMINED` 0, `FAILED` 1, `BEGAN` 2, `CANCELLED` 3, `ACTIVE` 4, `END` 5 | Every payload's `state`/`oldState` fields, so `state === State.ACTIVE` is ordinary, correct code. |\n| `Directions` | `RIGHT` 1, `LEFT` 2, `UP` 4, `DOWN` 8 a bitmask | `Gesture.Fling().direction()`. Four diagonal combinations exist by OR-ing two axis bits together (`UP \\| RIGHT`, and so on) but are not named on the public object, matching upstream. |\n| `PointerType` | `TOUCH` 0, `STYLUS` 1, `MOUSE` 2, `KEY` 3, `OTHER` 4 | Every payload's `pointerType`. Only `MOUSE` and `STYLUS` are ever actually reported on this platform. |\n| `HoverEffect` | `NONE` 0, `LIFT` 1, `HIGHLIGHT` 2 | `.effect()`/`hoverEffect` on `Gesture.Hover()`. Inert iOS's own pointer effect, inert in upstream's own web handler too. |\n| `MouseButton` | `LEFT` 1, `RIGHT` 2, `MIDDLE` 4, `BUTTON_4` 8, `BUTTON_5` 16, `ALL` 31 a bitmask | `.mouseButton()`. Inert, matching upstream off Web. |\n\n`HoverEffect` and `MouseButton` are exported as real data even though they are\ninert on this platform, for the same reason their knobs are accepted rather\nthan refused: both are already accepted-and-inert configuration, and a knob\nthat accepts a number while refusing the named constant for that number would\nbe incoherent. Every value in all five enums is pinned against\n`react-native-gesture-handler` 3.1.0's own source numbers.",
1007
1001
  },
1008
1002
  {
1009
- doc: "docs/platform-layer.md",
1010
- heading: "React Native content in native chrome",
1011
- text: 'An AdwHeaderBar slot wants a widget that knows its own size, which is what\n`IntrinsicContent` provides:\n\n```tsx\n<AdwHeaderBar\n start={\n <IntrinsicContent>\n <Text>{stack.length} deep</Text>\n </IntrinsicContent>\n }\n end={[\n <GtkButton\n key="home"\n iconName="go-home-symbolic"\n onClicked={reset}\n />,\n ]}\n/>\n```',
1003
+ doc: "docs/reference/gesture-handler.md",
1004
+ heading: "The re-exported components",
1005
+ text: "`ScrollView`, `FlatList`, `TextInput`, `Switch`, `Pressable`,\n`TouchableOpacity`, `TouchableHighlight` and `TouchableWithoutFeedback` are\nre-exported under RNGH's names as this platform's own components, unwrapped.\n\nUpstream builds each of these with\n`createNativeWrapper(RN.X, { disallowInterruption: true, shouldCancelWhenOutside: false })`\n— attaching a `NativeViewGestureHandler` so that its own arbitration knows\nabout the native scrolling or the native press underneath. On this platform\nthe responder system already **is** that arbitration: every one of these\ncomponents already speaks it, and `Gesture.Native()` is how an app declares a\ngesture over one of them explicitly when it needs to. The wrapper has nothing\nto add here, so the honest re-export is the component itself.",
1012
1006
  },
1013
1007
  {
1014
- doc: "docs/platform-layer.md",
1015
- heading: "Two ways to react to size",
1016
- text: 'Two mechanisms answer two different questions, and neither is a replacement\nfor the other:\n\n- **"Render different content at different widths"** `useWindowDimensions`\n (from `react-native`, portable, already exists). A resize triggers a React\n render, your component reads the new width, you return different JSX.\n This is the right and ONLY tool for anything that changes what is\n rendered — swapping a filter bar for a compact one, hiding a column,\n changing text.\n- **"Flip a widget property natively at a threshold, with no render at all"**\n — `AdwBreakpoint` + `AdwBreakpointBin`. `Adw.Breakpoint` is a condition\n (a size/aspect-ratio threshold) plus a set of property setters: when the\n condition starts holding, each setter writes its value onto its target\n object\'s property directly, through GObject, inside GTK\'s own allocation\n pass; when the condition stops holding, the setter puts the property back\n to whatever it held before. **No React commit, no Yoga pass, no JS\n callback runs for the flip itself**a resize costs nothing beyond what\n GTK\'s layout was already doing.\n\n `Adw.Breakpoint` is not a widget — verified against the real binding,\n `Adw.Breakpoint.prototype instanceof Gtk.Widget` is `false`; its\n prototype chain bottoms out at plain `GObject.Object`. It draws nothing\n and occupies no space, so it is exported raw (`AdwBreakpoint`, from\n `react-native-gtkx/adw`), the same way `GtkGestureClick` is: running it\n through `wrapReactNative` would hand it a Yoga node for something that\n is not a rectangle, which is a layout bug, not a convenience.\n `Adw.BreakpointBin` (`AdwBreakpointBin`) IS a real widget — a container\n that scopes breakpoints to its own child subtree instead of a whole\n window — and is wrapped normally, taking `style`/flex like anything else\n here.\n\n A breakpoint\'s setters may only target widgets INSIDE the\n `AdwBreakpointBin` they are attached to, never the bin itself — so the\n widget whose property you want to flip must be the bin\'s child:\n\n ```tsx\n import { Adw, AdwBreakpoint, AdwBreakpointBin } from "react-native-gtkx/adw"\n\n const splitViewRef = useRef<Adw.NavigationSplitView | null>(null)\n const breakpointRef = useRef<Adw.Breakpoint | null>(null)\n\n useEffect(() => {\n if (!splitViewRef.current || !breakpointRef.current) return\n const collapsed = new GObject.Value()\n collapsed.init(GObject.typeFromName("gboolean"))\n collapsed.setBoolean(true)\n breakpointRef.current.addSetter(splitViewRef.current, "collapsed", collapsed)\n }, [])\n\n <AdwBreakpointBin\n breakpoints={\n <AdwBreakpoint\n ref={breakpointRef}\n condition={Adw.BreakpointCondition.newLength(\n Adw.BreakpointConditionLengthType.MAX_WIDTH,\n 500,\n Adw.LengthUnit.SP,\n )}\n />\n }\n >\n <AdwNavigationSplitView ref={splitViewRef} …>…</AdwNavigationSplitView>\n </AdwBreakpointBin>\n ```\n\n `addSetter` wants a genuine, boxed `GObject.Value` — found empirically: a\n bare JS `true` fails a `G_IS_VALUE` assertion on the native side, it does\n not silently coerce. `createSidebarNavigator`\'s own `collapseWidth` (see\n below) is built on exactly this pair; reading `collapsed`/`showContent`\n back (e.g. to decide whether a click should also reveal content) is a\n plain native property read through the same ref, not React state so\n neither the flip nor a read of it costs a render.\n\nNo `useBreakpoint(condition) → boolean` hook exists, and none is planned:\nit would return a flag to JS and trigger a re-render on every crossing,\nwhich is precisely what `useWindowDimensions` already does — a second name\nfor the first mechanism, with none of the second\'s native-setter value.\nIf what you want is "my component\'s JSX changes", reach for\n`useWindowDimensions`; only reach for `AdwBreakpoint` when the thing that\nshould change is a widget property GTK itself owns, and you want that\nchange to cost nothing.',
1008
+ doc: "docs/reference/gesture-handler.md",
1009
+ heading: "What is not implemented",
1010
+ text: 'Every export listed below throws when usedon call, on render, or on\nproperty access, naming itself — rather than silently rendering its children\nwithout gestures attached. An import this subpath does not list at all fails\nearlier still, at bundle time, with the bundler\'s own "no export named X".',
1017
1011
  },
1018
1012
  {
1019
- doc: "docs/platform-layer.md",
1020
- heading: "Mixing with react-navigation",
1021
- text: "They compose, because the navigator is built on these primitives. Use\n`react-native-gtkx/navigation` for the app's structure and drop to\n`react-native-gtkx/gtk` and `react-native-gtkx/adw` where you need a widget the options do not cover —\nfor example a raw `GtkButton` in `headerButtons`, or a `GtkListBox` inside a\nscreen.\n\nKeeping portable code portable: put Linux-only UI behind a `.linux.tsx`\nplatform extension, or behind `Platform.select({ linux: … })`. Options a\nplatform does not understand are ignored, and in development the navigator\nwarns with the screen and option name rather than swallowing them silently.",
1013
+ doc: "docs/reference/gesture-handler.md",
1014
+ heading: "The legacy handler-component API (RNGH 1.x)",
1015
+ text: "`FlingGestureHandler`, `ForceTouchGestureHandler`, `LongPressGestureHandler`,\n`NativeViewGestureHandler`, `PanGestureHandler`, `PinchGestureHandler`,\n`RotationGestureHandler`, `TapGestureHandler` and `legacy_createNativeWrapper`\nall throw, naming themselves.\n\nThese are RNGH's 1.x component API —\n`<PanGestureHandler onGestureEvent={...}><View/></PanGestureHandler>`, with\nits own `onGestureEvent`/`onHandlerStateChange` event shape, its own\n`enabled`/`waitFor` prop plumbing and its own `createHandler` HOC which\nupstream deprecated years before it deprecated the builder spelling. The\nbuilder (`Gesture.Pan()` and its siblings) and the hook spelling\n(`usePanGesture()` and its siblings) are the two spellings implemented here,\nwhich is one more than upstream itself still recommends.\n`legacy_createNativeWrapper(Component, config)` attaches a\n`NativeViewGestureHandler` to an arbitrary component; it has nothing to add on\nthis platform for the same reason the re-exported components above do not\nneed it the responder system is already the arbitration it would register\nwith.",
1022
1016
  },
1023
1017
  {
1024
- doc: "docs/platform-layer.md",
1025
- heading: "Wrapping a widget we do not export",
1026
- text: 'The generated surface above covers every current `Gtk.Widget`/`Adw.Widget`\nsubclass gtkx binds, but "current" is doing work in that sentence: a gtkx\nrelease can add a widget before this package\'s generator has been re-run for\nit, and non-widget GI classes (an event controller, a filter, an adjustment)\nwere never candidates for the widget surface in the first place even though\na handful of them are occasionally worth putting inside RN layout too.\n`wrapReactNative` is how you reach either without waiting on us it is\ngeneric, so the widget\'s own props keep their types:\n\n```tsx\nimport { GtkPopover } from "@gtkx/jsx/gtk"\nimport { wrapReactNative } from "react-native-gtkx/common"\n\nconst Popover = wrapReactNative(GtkPopover)\n// <Popover style={{ width: 240 }} autohide … /> — `autohide` still typed\n```\n\n(`GtkPopover` here is already part of the generated surface — this is the\nsame mechanism `src/gtk/widgets.generated.ts` uses under the hood, just\napplied by hand. It stays useful the day gtkx binds something this package\nhas not regenerated for yet.)\n\nTwo lower-level forms exist for cases the wrapper does not fit:\n\n- `<Widget style={…}>` wrap an element you already have in hand;\n- `useWidgetLayout(ref, { style })` attach layout to a widget whose ref you\n own, with no wrapper component at all. Returns the GTK CSS class from the\n style\'s visual half, for you to pass to `cssClasses`.',
1018
+ doc: "docs/reference/gesture-handler.md",
1019
+ heading: "The native button family",
1020
+ text: "`BaseButton`, `RawButton`, `RectButton` and `BorderlessButton` all throw,\nnaming themselves.\n\nThese are not RN components with a handler attachedthey are RNGH's own\nnative button views, implemented in Java and Objective-C, with an Android\nripple, `rippleColor`/`rippleRadius`, `borderless` drawable selection, an\n`exclusive` group, and an `activeOpacity` applied by the native view rather\nthan by style. No GTK widget has that set of semantics, and there is no way to\nfake the rippleany implementation would be a `Pressable` wearing another\ncomponent's name.",
1027
1021
  },
1028
1022
  {
1029
- doc: "docs/platform-layer.md",
1030
- heading: "The escape hatch",
1031
- text: "If something is missing, reach the widget directly:\n\n```tsx\nconst viewRef = useRef<Adw.NavigationView | null>(null)\n<NavigationStack ref={viewRef} stack={stack}>…</NavigationStack>\n// viewRef.current is the real Adw.NavigationView\n```\n\nThere is deliberately no wall here. A missing convenience should cost you one\nline, not a fork.",
1023
+ doc: "docs/reference/gesture-handler.md",
1024
+ heading: "RefreshControl, Touchable and TouchableNativeFeedback",
1025
+ text: "All three throw, naming themselves.\n\n`TouchableNativeFeedback` is Android's ripple by another name.\n`Touchable` is React Native's own deprecated mixin. `RefreshControl` is\npull-to-refresh, which needs a scroll gesture this platform's `ScrollView`\ndoes not expose and a spinner widget this platform does not have.",
1032
1026
  },
1033
1027
  {
1034
- doc: "docs/platform-layer.md",
1035
- heading: "Related",
1036
- text: "- [API v1](api.md) — the portable React Native surface.\n- [Navigation research](research/navigation-extensibility.md)how the\n adapter maps react-navigation onto these primitives.\n- [What we need from gtkx](upstream-gtkx.md)the upstream agenda.",
1028
+ doc: "docs/reference/gesture-handler.md",
1029
+ heading: "The three new-API pieces that don't apply here",
1030
+ text: '`GestureDetectorType`, `InterceptingGestureDetector` and\n`VirtualGestureDetector` all throw, naming themselves, for three separate\nreasons:\n\n- **`GestureDetectorType`** is a type upstream, not a value. Type positions\n never reach this module at all — the alias is a bundler alias, so `tsc`\n resolves the real package\'s types from `node_modules` — so a runtime value\n under this name could only be reached by code that has already gone wrong.\n- **`InterceptingGestureDetector`** intercepts events destined for views below\n it. Doing that here would mean claiming a GTK sequence before deciding\n whether to keep it, and a claim on this platform is irrevocable — the\n "intercept, look, maybe give it back" shape has no GTK equivalent.\n- **`VirtualGestureDetector`** drives a gesture with no view at all. The\n handler-tag registry behind `GestureStateManager` answers "which mounted\n recognizer does this number mean", not "mint a recognizer with nothing to\n measure and no widget to attach a controller to" every recognizer on this\n platform is still built by a mounted `GestureDetector` wrapping exactly one\n child.',
1031
+ },
1032
+ {
1033
+ doc: "docs/reference/gesture-handler.md",
1034
+ heading: "The 2.x legacy aliases",
1035
+ text: "`LegacyScrollView`, `LegacyFlatList`, `LegacyTextInput`, `LegacySwitch`,\n`LegacyPressable`, `LegacyText`, `LegacyRawButton`, `LegacyBaseButton`,\n`LegacyRectButton`, `LegacyBorderlessButton`, `LegacyRefreshControl` and\n`LegacyDrawerLayoutAndroid` all throw, naming themselves.\n\nEach is 3.x's escape hatch back to its 2.x implementation of a component whose\n3.x spelling either already works here under its modern name, or is refused\nabove with its own reason. Where the modern name works, the legacy alias\nwould carry a promise this platform cannot keep — \"this behaves like 2.x\ndid\" — since 2.x's own behavior was never implemented here to differ from.\nWhere the modern name is refused, the alias inherits that refusal.\n`LegacyDrawerLayoutAndroid` is refused twice over: React Native itself does\nnot ship `DrawerLayoutAndroid` off Android, and `@react-navigation/drawer`\nreaches for `react-native-drawer-layout` instead, which runs on this\nplatform.",
1036
+ },
1037
+ {
1038
+ doc: "docs/reference/globals.md",
1039
+ heading: "Already native, nothing to install",
1040
+ text: "Node already provides `fetch`/`Headers`/`Request`/`Response`, `Blob`/`File`,\n`WebSocket`, `URL`/`URLSearchParams`, `AbortController`/`AbortSignal`,\n`structuredClone`, `TextEncoder`/`TextDecoder`, `atob`/`btoa`,\n`queueMicrotask`, `setImmediate`/`clearImmediate`, a monotonic `performance`,\n`crypto`, `DOMException`, and `console` (including `group`/`groupCollapsed`/\n`groupEnd`, which RN's own console polyfill only ever adds on a native\nruntime). `FormData` is native too, but not RN-compatible in one specific\nway: it does not understand react-native's own file-entry shape\n(`formData.append('photo', { uri, type, name })`) — an object there is\ncoerced to the literal string `\"[object Object]\"` instead of attaching a\nfile. `XMLHttpRequest` and `FileReader` are not Node-native at all and are\nnot installed by this platform, unlike RN, which installs both\nunconditionally; reach for `fetch` and `Blob`'s own `.text()`/\n`.arrayBuffer()`/`.stream()` instead.",
1041
+ },
1042
+ {
1043
+ doc: "docs/reference/globals.md",
1044
+ heading: "Installed for parity",
1045
+ text: "Each of these is installed only if nothing already provides it, so an\nexisting global always wins:\n\n- **`window = globalThis`, `self = globalThis`** — the same thing RN's own\n bootstrap does first. An isomorphic library's `typeof window !==\n\"undefined\"` check — usually meaning \"not a server context, safe to run\n browser-shaped init\" — reads the same way here as on any other RN\n platform.\n- **`navigator.product = \"ReactNative\"`** — the ecosystem's standard\n environment-detection value. Node already ships a minimal `navigator`\n (`.userAgent` only) from version 21 on; this adds `product` next to it\n rather than replacing the object, matching RN's own fallback behavior\n exactly.\n- **`requestIdleCallback`/`cancelIdleCallback`** — the standard web-fallback\n shape every userland polyfill uses: fires on the next macrotask, reports a\n fixed 50&nbsp;ms budget through `timeRemaining()`, and `didTimeout` is\n always `false`. This is \"run this off the current tick, eventually,\" not a\n real idle-scheduling primitive — code that depends on genuine idle\n detection should not rely on it.\n- **`global.alert`** — forwards a single string to `Alert.alert('Alert',\ntext)`, against this platform's own `Alert` module (see\n [APIs](apis.md#alert)).\n- **`ErrorUtils`** — `setGlobalHandler`/`reportError`/`reportFatalError`/\n `applyWithGuard`/`guard`, a faithful port of RN's own polyfill. The default\n handler rethrows, exactly RN's un-hooked behavior. Both toolchains provide\n it, so code that expects `global.ErrorUtils` to exist (which several\n RN-ecosystem libraries do) finds it either way.\n- **`requestAnimationFrame`/`cancelAnimationFrame`** — installed as globals,\n not module exports, exactly as RN installs them from its own bootstrap\n rather than exporting them from `\"react-native\"`. Both ride the same frame\n clock `Animated` and the Reanimated-compatible surface already share, not a\n second timer. A call returns an id; the callback receives a monotonic,\n high-resolution timestamp; a callback requested while a batch is already\n running lands on the next frame, never the one currently flushing;\n cancelling is silent, including for an unknown or already-delivered handle;\n and one callback throwing is reported without stopping its siblings in the\n same batch. Differs from react-native only in mechanism, not in behavior —\n there is no native per-platform frame source on a Linux desktop, so this\n rides the same clock `Animated` runs on, the way the DOM's own\n `requestAnimationFrame` stands in for it on react-native-web.",
1046
+ },
1047
+ {
1048
+ doc: "docs/reference/globals.md",
1049
+ heading: "`__DEV__`",
1050
+ text: "Provided by the bundler, not by this module. The vite preset defines it from\nvite's own build mode; the Metro path gets it from the app's own stock\n`@react-native/metro-config` preset, independent of this platform's own\nMetro wrapper.",
1037
1051
  },
1038
1052
  {
1039
- doc: "docs/getting-started.md",
1053
+ doc: "docs/reference/globals.md",
1054
+ heading: "Not installed, by architecture",
1055
+ text: "React Native's Fabric-era DOM-compatibility globals — `Node`, `Element`,\n`HTMLElement`, `Document`, `Event`, `EventTarget`, `CustomEvent`,\n`DOMRect(ReadOnly/List)`, `HTMLCollection`, `NodeList` — exist to back\nFabric's DOM-traversal API over its C++ shadow tree. This platform has\nneither Fabric nor a shadow tree of its own — its React reconciler drives\nGTK widgets and the Yoga layout tree directly — so there is no shadow tree\nfor a DOM-shaped facade to expose, and none of these globals are installed.",
1056
+ },
1057
+ {
1058
+ doc: "docs/reference/globals.md",
1059
+ heading: "The runtime itself",
1060
+ text: 'Every module of npm and Node is available at runtime — `fs`, `sqlite`,\nnative addons — so a "native module" here is written as an ordinary Node\nmodule rather than as platform-specific native code. An RN library whose\nnative side is genuinely iOS/Android code (rather than pure JavaScript) does\nnot run here. The package itself ships compiled — ESM plus `.d.ts` files,\nsources embedded in the maps — and requires Node ≥ 24, the floor both the\ngtkx runtime and the `run-linux` host rely on.',
1061
+ },
1062
+ {
1063
+ doc: "docs/reference/navigation.md",
1040
1064
  heading: "Requirements",
1041
- text: "- Linux (x64/arm64, glibc), GTK4 4.20, libadwaita 1.8 (Ubuntu 26.04+, Fedora 43+);\n- Node.js 24;\n- dev packages: `sudo apt install libgtk-4-dev libadwaita-1-dev` (Ubuntu).",
1065
+ text: 'The package peers optionally on `@react-navigation/native` (v8), which must\nbe installed alongside it.\n\n`@react-navigation/native@8` itself peers on `react-native: "*"` — unlike\n`@react-navigation/core@8`, which declares no `react-native` peer at all. An\napp with no `react-native` package anywhere in its tree (a vite+gtkx app\nwith no Metro side, for example) gets an unmet-peer-dependency warning from\n`npm install` for it. The warning is harmless: react-native-gtkx never\nimports anything from the `react-native` package, so nothing at runtime\nactually needs it present.\n\n`react-native-gtkx/navigation` exports exactly two factories —\n`createStackNavigator` and `createSidebarNavigator` — and the option/prop/\nevent types around them. The rest of the react-navigation surface\n(`useNavigation`, `useRoute`, `useFocusEffect`, `useIsFocused`,\n`useNavigationContainerRef`, `CommonActions`, `StackActions`,\n`usePreventRemove`, `NavigationContainer`, and everything else) comes from\n`@react-navigation/native` directly, not from this package.',
1042
1066
  },
1043
1067
  {
1044
- doc: "docs/getting-started.md",
1045
- heading: "New project from the template",
1046
- text: "```bash\nnpx degit itsmepetrov/react-native-gtkx/template my-app && cd my-app\nnpm install\nnpm run dev # window with Fast Refresh (edits apply without a restart)\nnpm run build && npm start # production bundle, runs with plain node\n```\n\nMeasured in a clean Ubuntu 26.04 container: 63 seconds from install to a window on screen.",
1068
+ doc: "docs/reference/navigation.md",
1069
+ heading: "Window chrome",
1070
+ text: "Both navigators' header bars stand in for the window's own title bar, so\nthe app should run with content chrome:\n\n```tsx\nAppRegistry.runApplication(name, { ..., chrome: \"content\" })\n```\n\nRunning with the default system chrome instead doubles the title bar,\nsince the pages already bring their own header bars. In that case, each\nnavigator logs a one-time development warning naming the fix.",
1047
1071
  },
1048
1072
  {
1049
- doc: "docs/getting-started.md",
1050
- heading: "How it works",
1051
- text: '```\nyour code (react-native API)\n └─ vite preset: aliases react-native react-native-gtkx, platform\n extensions .linux.tsx .native.tsx base\n └─ react-native-gtkx: Yoga (WASM) computes flexbox; styles are split into\n layout (Yoga) and visual (GTK CSS); coordinates are applied to\n real GTK widgets\n └─ gtkx: React reconciler GTK4 via FFI\n```\n\nThe entry point is the same as in RN:\n\n```tsx\nimport { AppRegistry, StyleSheet, Text, View } from "react-native"\n\nconst App = () => (\n <View style={styles.screen}>\n <Text style={styles.title}>Hello, GNOME!</Text>\n </View>\n)\n\nconst styles = StyleSheet.create({\n screen: { flex: 1, alignItems: "center", justifyContent: "center" },\n title: { fontSize: 24, fontWeight: "700" },\n})\n\nAppRegistry.registerComponent("app", () => App)\nAppRegistry.runApplication("app", { title: "My App", width: 800, height: 600 })\n```\n\n`runApplication` accepts desktop parameters (`title`, `width`, `height`) — the only extension over the RN signature.',
1073
+ doc: "docs/reference/navigation.md",
1074
+ heading: "Stack navigator",
1075
+ text: '![The gallery\'s Adwaita stack section: a real Adw.NavigationView push, with a native header-bar back button the same primitive react-native-gtkx/navigation\'s stack navigator builds on.](../shots/gallery/adwaita-stack.png)\n\n_This demo bypasses react-navigation entirely (its own `useState` router); it\nonly proves the underlying native primitive the stack navigator above is\nbuilt on._\n\n`createStackNavigator()` returns a `Navigator`/`Screen` pair used the same\nway as `@react-navigation/native-stack`\'s:\n\n```tsx\nimport { NavigationContainer } from "@react-navigation/native"\nimport { createStackNavigator } from "react-native-gtkx/navigation"\n\nconst Stack = createStackNavigator()\n\nconst App = () => (\n <NavigationContainer>\n <Stack.Navigator>\n <Stack.Screen\n name="Home"\n component={HomeScreen}\n />\n <Stack.Screen\n name="Details"\n component={DetailsScreen}\n options={{ title: "Details page" }}\n />\n </Stack.Navigator>\n </NavigationContainer>\n)\n```',
1052
1076
  },
1053
1077
  {
1054
- doc: "docs/getting-started.md",
1055
- heading: "Add Linux to an existing React Native app",
1056
- text: 'Linux is an [out-of-tree platform](https://reactnative.dev/docs/out-of-tree-platforms)\n(the react-native-windows/macOS model): your app keeps its ios/ and\nandroid/ folders, its Metro/Babel toolchain, and gains one more target.\nFour steps:\n\n1. **Install the platform package:**\n\n ```bash\n npm install react-native-gtkx\n ```\n\n Its own `react-native.config.js` declares the `linux` platform and the\n `run-linux` command nothing to declare app-side.\n\n2. **Wrap your Metro config** (`metro.config.js`):\n\n ```js\n const { getDefaultConfig } = require("@react-native/metro-config")\n const { withLinuxPlatform } = require("react-native-gtkx/metro")\n\n module.exports = withLinuxPlatform(getDefaultConfig(__dirname))\n ```\n\n The wrap adds the platform (`.linux.tsx` extensions,\n `Platform.OS === "linux"`), redirects `react-native` imports to the\n platform package, and keeps host-side modules (GTK bindings, react,\n yoga) out of the bundle. Babel stays completely stock.\n\n3. **Add `gtkx.config.ts`** with the GTK application id:\n\n ```ts\n import { defineConfig } from "@gtkx/config"\n\n export default defineConfig({\n libraries: ["Gtk-4.0", "Adw-1"],\n applicationId: "com.example.myapp",\n })\n ```\n\n4. **Start the app from the entry** on desktop the entry launches the\n app itself (the same pattern as react-native-web\'s `index.web.js`):\n\n ```js\n // index.js, after AppRegistry.registerComponent(...)\n if (Platform.OS === "linux") {\n AppRegistry.runApplication(appName, {\n title: "My App",\n width: 800,\n height: 600,\n })\n }\n ```\n\nRun it:\n\n```bash\nnpx react-native run-linux # release bundle\nnpx react-native run-linux --dev # Metro dev server + Fast Refresh\n```\n\nThe command ensures the gtkx codegen store, bundles with Metro for\n`--platform linux` and opens the window. With `--dev` it starts (or\nreuses) the Metro dev server and edits apply to the live window with\ncomponent state preserved; syntax errors print readably in the terminal\nand the app recovers on the next successful build. **Ctrl+Shift+D** (the\nreact-native-windows shortcut the desktop stand-in for the shake\ngesture) opens the Dev Menu: Reload plus any entries the app registers\nvia `DevSettings.addMenuItem`. `examples/rn-app` is a complete cli-init\napp with all three platforms wired this way.\n\n`run-linux` always runs what it builds for a release build that stops\nshort of opening a window (packaging, CI, handing a bundle to someone\nelse\'s machine), use `build-linux` instead; see\n[Shipping an app](#shipping-an-app) below for what it produces and what\nrunning it later needs.\n\nNotes for typed code: add an `env.d.ts` with\n`import "react-native-gtkx/types"` it augments the stock `react-native`\ntypes so `Platform.select({ linux: ... })` typechecks, and `Pressable`\'s\nstate callback accepts `hovered` (declared optional a component shared\nwith ios/android gets `undefined` there, so write\n`hovered && styles.hovered`). Future platform-specific props land in the\nsame file. One thing augmentation\ncannot teach is `Platform.OS === "linux"` (property types do not merge) —\nuse `Platform.select` in typed code. Deep imports\n(`react-native/Libraries/...`) are not supported only the public\n`react-native` surface.',
1078
+ doc: "docs/reference/navigation.md",
1079
+ heading: "Screen options",
1080
+ text: '- **`title`** (`string`, default: route name) — Header bar title.\n- **`headerShown`** (`boolean`, default `true`) Shows the header bar for\n this screen.\n- **`headerButtons`** (`HeaderButton[]`) Native buttons packed at the end\n of the header bar, after `headerRight`. Each button is `{ id, icon,\ntooltip, onPress }`; `icon` is an Adwaita symbolic icon name.\n- **`headerLeft`** (`() => ReactNode`) Content packed at the start of the\n header bar, in an intrinsic-size layout root — the content\'s own Yoga\n size is the slot size.\n- **`headerRight`** (`() => ReactNode`) Content packed at the end of the\n header bar, before `headerButtons`.\n- **`gestureEnabled`** (`boolean`, default `true`) `false` disables the\n native back button, Escape and the back gesture for this screen.\n Programmatic `goBack` still works; this is also the mechanism behind\n `usePreventRemove` a prevented route reports the same disabled state,\n so no native pop can race react-navigation state, and the route pops\n once the app lifts the guard.\n- **`animation`** (`string`, default `"default"`) Differs from\n react-navigation: GTK has exactly one transition style, so this\n collapses to a boolean. `"none"` turns transitions off; any other value\n including native-stack\'s own style names such as\n `"slide_from_bottom"` or `"fade"` turns transitions on and plays the\n standard Adwaita transition instead of the one requested. A\n non-`"none"`/`"default"` value still animates (it is not silently\n treated as `"none"`) and logs a development warning once.\n\n`animation` is a property of the whole view, not a per-page one, so there\nis no per-screen granularity: the value used is read from whichever screen\nis currently on top of the visible stack, recomputed on every navigation.\nSetting it once via `screenOptions` the same value for every screen is\nthe reliable way to use it; the per-screen case only matters when\ndifferent screens genuinely disagree, and even then only the active\nscreen\'s value is observed. Interactive swipe-back gestures always animate\nregardless of this setting, an Adwaita behavior that is not overridable\nhere.\n\nWhen `headerShown` is `false`, the screen\'s content fills the page\ndirectly, with no header bar; otherwise it renders inside the header bar\'s\ncontent area. Each screen mounts its own layout root inside the page, so\nthe page\'s content allocation is exactly that screen\'s viewport.\n\nDiffers from react-navigation: a full custom header replacement\n(`@react-navigation/native-stack`\'s `header` option) is not implemented —\n`headerLeft`, `headerRight` and `headerButtons` compose within the\nstandard header bar instead. Deep-link `url` events never fire on\ndesktop; see [APIs](apis.md#linking) for `Linking`.',
1057
1081
  },
1058
1082
  {
1059
- doc: "docs/getting-started.md",
1060
- heading: "Navigation",
1061
- text: "Multi-screen apps use the standard react-navigation API with a native\nAdwaita stack navigator: install `@react-navigation/native` and import\n`createStackNavigator` from `react-native-gtkx/navigation` — pages render\nas `Adw.NavigationPage` with the HeaderBar back button wired to\nreact-navigation state. See [docs/api.md](api.md#navigation-react-native-gtkxnavigation), and\n[docs/research/navigation-extensibility.md](research/navigation-extensibility.md)\nfor porting an existing react-navigation app (which options carry over,\nwhich are silently ignored today, and what the desktop cannot mean).",
1083
+ doc: "docs/reference/navigation.md",
1084
+ heading: "Transition events",
1085
+ text: "The stack navigator emits two events on a screen's `navigation` object,\nmatching `@react-navigation/stack` and `@react-navigation/native-stack`:\n\n- **`transitionStart`** (`{ data: { closing: boolean } }`) — Fires when a\n push/pop/replace transition starts, once per involved route (not once\n per gesture or tap). `closing` is `false` for the route becoming\n visible, `true` for the route leaving the visible stack.\n- **`transitionEnd`** (`{ data: { closing: boolean } }`) Fires when the\n transition settles. Tied to `AdwNavigationPage`'s own `shown`/`hidden`\n signals: it fires on `shown` for the entering screen and on `hidden` for\n the leaving screen. `transitionDuration` (default 400 ms) is a fallback\n only, used when a page's own signal never arrives — a signal-less\n environment, or an intermediate screen skipped entirely by a multi-hop\n pop. When transitions are not animated, the real signals still fire\n immediately, so `transitionEnd` is never delayed by the fallback window.\n\nA screen that stays mounted without actually entering or leaving the\nvisible stack — the screen underneath a push, for example — receives\nneither event, matching upstream.\n\nDiffers from react-navigation: native pops (the back button, Escape, the\nback gesture) do not fire either event today. A user-driven pop is\nhandled by the widget itself before the adapter is told about it, so there\nis nothing to hook a `transitionStart` into; only programmatic navigation\n(`navigate`, `goBack`, `dispatch`, …) fires these events.",
1062
1086
  },
1063
1087
  {
1064
- doc: "docs/getting-started.md",
1065
- heading: "Svg",
1066
- text: '`<Svg>`/`<Path>`/`<Circle>` and the rest of the vector-graphics API come from\n`react-native-svg`, not from `react-native-gtkx` itself matching every\nother platform, where `react-native-svg` is a separate package too (RN has\nno built-in `Svg`). See [docs/api.md](api.md#svg) for the component set and\n[the compat-subpath section](api.md#react-native-svg-compatibility-react-native-gtkxsvg)\nfor how both presets alias the bare `react-native-svg` import to it.\n\nThat alias is a bundler-level rewrite, so TypeScript still needs its own\nanswer for the specifier `"react-native-svg"` an unresolved import in the\neditor even though the build works fine. Which fix applies depends on what\nthe project targets:\n\n- **Also ships to iOS/Android/web**: install the real `react-native-svg` —\n the app needs it on those platforms regardless. `react-native-svg` ships\n its own `.d.ts` (no separate `@types` package exists or is needed), so\n TypeScript resolves real, complete types for the specifier; the Linux\n build never actually executes that package\'s code the preset rewrites\n the import to `react-native-gtkx/svg` before it reaches Node. Nothing\n react-native-gtkx-specific to configure.\n- **Linux-only project** (the template, or an app with no mobile target):\n add `react-native-svg` as a **devDependency purely for its types** —\n `npm install -D react-native-svg`. This is the ordinary fix for a\n bundler-alias setup once the aliased name has no real package installed —\n the same shape as react-native-web\'s own TypeScript guidance (install a\n real, type-bearing package alongside the alias rather than fabricate\n one). Side benefit: if this package\'s compat surface ever drifts from\n upstream `react-native-svg`\'s props (see the "Deliberate gaps" note in\n `packages/react-native-gtkx/src/svg-compat/index.ts`), the mismatch shows\n up as a type error instead of compiling silently.\n\nWe deliberately did not ship an ambient `declare module "react-native-svg"`\n— the trick `react-native-gtkx/types` uses to teach the stock `react-native`\ntypes about the `linux` platform. That works there because it only\n_augments_ an already-resolved module (interfaces merge). Here the module\ndoes not resolve at all without one of the two installs above, so the shim\nwould have to declare the whole module unconditionally to help — and a\nproject that installs the real `react-native-svg` later (adding a mobile\ntarget) would then carry two declarations of the same module, the shim and\nthe real package\'s own, colliding. Installing the real package, even only\nas a devDependency, never has that problem: there is only ever one\ndeclaration of `"react-native-svg"` in play.',
1088
+ doc: "docs/reference/navigation.md",
1089
+ heading: "Sidebar navigator",
1090
+ text: '`createSidebarNavigator()` is the desktop equivalent of a drawer navigator,\nbuilt on `Adw.NavigationSplitView`: a persistent native sidebar (an\n`AdwActionRow` per screen, in a `GtkListBox` with Adwaita\'s\n`navigation-sidebar` styling) selects between parallel screens `TabRouter`\nsemantics, not a stack.\n\n```tsx\nimport { createSidebarNavigator } from "react-native-gtkx/navigation"\n\nconst Sidebar = createSidebarNavigator()\n\nconst App = () => (\n <NavigationContainer>\n <Sidebar.Navigator sidebarTitle="Mail">\n <Sidebar.Screen\n name="Inbox"\n component={InboxScreen}\n options={{ icon: "mail-symbolic" }}\n />\n <Sidebar.Screen\n name="Trash"\n component={TrashScreen}\n options={{ icon: "user-trash-symbolic" }}\n />\n </Sidebar.Navigator>\n </NavigationContainer>\n)\n```',
1067
1091
  },
1068
1092
  {
1069
- doc: "docs/getting-started.md",
1070
- heading: "Metro or vite?",
1071
- text: "- **Adding Linux to an existing RN app** (ios/android + Metro): the\n section above standard RN toolchain end to end,\n `run-linux --dev` for Fast Refresh.\n- **Linux-first project**: the template with the vite preset\n (`react-native-gtkx/vite`; `gtkx dev` gives Fast Refresh, builds are\n single-file bundles). Both paths consume the same published package.",
1093
+ doc: "docs/reference/navigation.md",
1094
+ heading: "Navigator props",
1095
+ text: "- **`sidebarTitle`** (`string`, default `\"Sidebar\"`) Title of the\n sidebar pane's header bar.\n- **`headerButtons`** (`HeaderButton[]`) — Buttons packed at the end of the\n content header bar; a screen's own `headerButtons` option overrides this\n entirely for that screen.\n- **`sidebarHeaderLeft` / `sidebarHeaderRight`** (`() => ReactNode`) —\n Content packed at the start/end of the sidebar pane's own header bar —\n distinct from the content header's `headerLeft`/`headerRight`, which are\n per-screen options, because one sidebar pane is shared by every screen.\n Mounted through the same intrinsic content root as the content header,\n so it lays out as a horizontal, content-hugging cluster flush with\n natively packed buttons.\n- **`sidebarHeaderTitle`** (`() => ReactNode`) — Replaces the sidebar\n header bar's title widget (a search entry, a switcher). Left unset,\n `sidebarTitle` renders as a plain label.\n- **`collapseWidth`** (`number`, sp; unset by default) — Width below which\n the split view collapses to the sidebar or the content pane alone,\n through a native `Adw.Breakpoint`. Unset by default: no breakpoint is\n mounted at all, so an app that never sets this sees no behavior change.\n- **`minWidth` / `minHeight`** (`number`, px; default `360` / `294`) — The\n narrowest size the sidebar navigator's UI supports, applied to the\n breakpoint container `collapseWidth` mounts. Ignored when\n `collapseWidth` is unset, since no container exists then. The default is\n GNOME's own adaptive floor.\n- **`sidebarContent`** (`(props: SidebarContentProps) => ReactNode`) —\n Replaces the entire sidebar pane body.\n\n`collapseWidth` is not driven by React state or `useWindowDimensions`: the\nproperty flip happens inside GTK's own allocation pass, at no cost of a\nReact render for the resize itself.\n\nAdwaita cannot measure a breakpoint container on its own — what it holds\nchanges with the breakpoints — so it otherwise reports a minimum size of\nzero and warns that a width/height request must be set. Left at the\ndefault, this is not an issue; an app whose content header bar needs more\nroom than the default (a segmented control as `headerTitle`, for example,\ncosts roughly 110 px on its own and cannot ellipsize the way a plain title\nlabel can) must raise `minWidth`/`minHeight` — measured against the pane's\nown content, not guessed. Setting it too low does not fail loudly: the\nwindow resizes past what the pane can draw, and Adwaita clips the pane\ninstead of adapting it (an `AdwNavigationSplitView exceeds\nAdwBreakpointBin width` message in the system journal, felt as content\nrunning off the edge). The sidebar pane's own width is separately bounded\nbetween 180 and 280 px regardless of `collapseWidth`.",
1072
1096
  },
1073
1097
  {
1074
- doc: "docs/getting-started.md",
1075
- heading: "The React Compiler is on by default (vite path)",
1076
- text: '`gtkx dev` and `gtkx build` run the\n[React Compiler](https://react.dev/learn/react-compiler) over every source\nfile in the project — never `node_modules`. Nothing switches it on; it is on\nunless `gtkx.config.ts` switches it off:\n\n```ts\nexport default defineConfig({\n libraries: ["Gtk-4.0", "Adw-1"],\n applicationId: "com.example.myapp",\n reactCompiler: false,\n})\n```\n\nLeaving the option out and setting it to `true` mean the same thing — only an\nexplicit `false` disables it. The Metro path (`run-linux` / `build-linux`)\nkeeps the app\'s stock Babel preset and does not run the compiler at all.\n\n**If a ported app misbehaves in a way that smells like stale rendering, set\n`reactCompiler: false` and see whether the symptom goes away.** One line, and\nit tells you which half of the system to debug worth knowing about up\nfront, because the symptom does not look like a compiler.\n\nThe expensive version of this: a component that reads mutable module-level\nstate during render has that read memoised. A render counter built on\n`readCounter("loop")` a function taking no reactive input is computed\nonce, and the JSX built from it is reused forever. The component re-renders\nfourteen times and shows the mount value every time. It looks exactly like a\nbroken counter, and it is not: it is a working counter behind a cached\nrender.\n\nOn React Native the compiler is opt-in, so an app that does not follow the\n[Rules of React](https://react.dev/reference/rules) still works. Here it is\non, so the same violations become visible misbehaviourand on a platform\nwhere everything else is new too, that reads as _our_ bug. Flipping\n`reactCompiler` to `false` settles it. Once it is settled the fix is the\nRules-of-React one (move the read into state, a ref or a hook), not leaving\nthe compiler off.\n\nReanimated shared values have their own spelling for the same reason:\n`sharedValue.value = x` and `sharedValue.set(x)` both work here, but only\n`.get()`/`.set()` passes compiler-aware lint see\n[api.md](api.md#writing-a-shared-value-value-or-set).',
1098
+ doc: "docs/reference/navigation.md",
1099
+ heading: "Screen options",
1100
+ text: '- **`title`** (`string`, default: route name) Sidebar row and content\n header bar title.\n- **`icon`** (`string`) Adwaita symbolic icon name for the row\'s prefix.\n Ignored when `color` is also set a row shows a colored dot or an icon,\n never both.\n- **`color`** (`string`) CSS color for a colored-dot prefix, replacing\n `icon`. `color` wins when both are set.\n- **`count`** (`number`) — Badge shown as the row\'s suffix. Hidden when\n `0` or unset.\n- **`headerLeft` / `headerRight`** (`() => ReactNode`) — Content header bar\n start/end, per screen a filter toggle group for a list, a back button\n plus star/trash for an open item.\n- **`headerTitle`** (`() => ReactNode`) — Replaces the content header\n bar\'s title widget for this screen. Left unset, the header bar shows the\n page\'s own title automatically.\n- **`headerButtons`** (`HeaderButton[]`) Overrides the navigator-level\n `headerButtons` prop for this screen.\n- **`contentLayout`** (`"react-native" | "widget"`, default\n `"react-native"`) — What the screen\'s body is. `"react-native"` mounts\n it in a Yoga layout root that fills the pane, so `<View style={{ flex:\n1 }}>` behaves the way it does anywhere else. `"widget"` packs the body\n into the page directly, with no layout root in between, for a screen\n whose body is a GTK widget tree GTK\'s own sizing (`vexpand`, a list\'s\n natural height) then applies normally. Under the default, a widget tree\n collapses instead, and quietly: every widget becomes a single Yoga leaf\n measured for its own natural size, so the container renders its first\n child, drops the rest, and reports the roughly 1 px it can shrink to,\n with no error anywhere. Mixing is per screen, not per subtree a\n `"widget"` screen that wants React Native content somewhere inside it\n wraps that part in `SlotContent` itself.\n- **`sidebarRow`** (`() => ReactNode`) Draws the row directly instead of\n letting `title`/`icon`/`color`/`count` compose one. See\n [Building sidebar rows](#building-sidebar-rows) below.\n- **`group`** (`string`) Section this row belongs to. See\n [Grouping rows](#grouping-rows) below.\n\nA screen changes its own header shape from inside itself by calling\n`navigation.setOptions({ headerLeft, headerRight, headerTitle })` in an\neffect keyed on whatever local state decides the shape — no navigator API\nbeyond the options themselves is involved. `setOptions` merges into the\npreviously resolved options rather than replacing them: a call that omits\n`headerRight` does not clear a `headerRight` a previous call set, it\nleaves it in place. A screen that flips between header shapes must give\nevery one of `headerLeft`, `headerRight`, `headerTitle` and `headerButtons`\nan explicit value on every call — `undefined` counts as a real overwrite,\nan absent key does not.',
1077
1101
  },
1078
1102
  {
1079
- doc: "docs/getting-started.md",
1080
- heading: "Shipping an app",
1081
- text: "The two paths get you from source to something installable differently,\nbecause they take different positions on what stays out of the bundle.\n\n**vite path** (`gtkx build`): everything except the native GTK addon is\ninlined into one file. `dist/bundle.js` + `dist/gtkx.node` (plus\n`dist/gschemas.compiled` if the app declares a GSettings schema — the\nbundle's own banner points `GSETTINGS_SCHEMA_DIR` at its own directory) is\nthe whole runtime: copy those anywhere with Node ≥24, GTK4 ≥4.20 and\nlibadwaita ≥1.8, and `node bundle.js` runs it. No `node_modules` involved.\n\n**Metro path** (`react-native build-linux`): the release counterpart to\n`run-linux` that iOS, Android and react-native-windows already have and\nthis platform did not until nowit bundles with Metro and stops, instead\nof bundling and immediately running like `run-linux` does:\n\n```bash\nnpx react-native build-linux # writes dist/main.jsbundle\n```\n\nThis is **not** self-contained, unlike the vite path. Metro deliberately\nkeeps `@gtkx/*`, `react` and `yoga-layout` out of the bundle they have to\nbe the exact instances the Node+GTK host loads, not a second copy Metro\ninlines (see `packages/react-native-gtkx/src/metro/index.ts`,\n`HOST_MODULE_EXTERNALS`, for why). So running `dist/main.jsbundle` needs,\non top of Node ≥24/GTK4/libadwaita, a real `node_modules` with\n`react-native-gtkx` installed and the app's `gtkx.config.ts` present at the\nworking directory:\n\n```bash\nnode node_modules/react-native-gtkx/dist/runner/host.js dist/main.jsbundle\n```\n\n(run from the app root the config loader reads `gtkx.config.ts` from the\ncurrent directory, exactly like `run-linux` itself). Any ordinary\n`npm install` of the app already has that `node_modules`, so this is a fine\nway to run a release bundle from a checkout. It is a bad thing to _ship_:\npackaging it means shipping the closure too, and that closure is not the\nhandful of runtime modules it sounds like. Measured on the release that did\nexactly this (v0.2.0-alpha.1's `hn-app` `.deb`): **10,515 files, 206 MiB\ninstalled** to run a 369 KB bundle, because `react-native-gtkx`'s install\ndrags its build toolchain along `typescript`, `@swc`, `rolldown`,\n`@babel`, `lightningcss`, all of it landing under `/opt` on a user's\nmachine. `scripts/build-deb.ts` no longer packages this shape at all.\n\nThat is the **default** artifact, and it is the only one that carries the\n`node_modules` caveat. `--standalone` below removes it entirely: the same\nMetro build, emitted as one self-contained file that runs on a system Node\nwith nothing installed beside it the vite path's shape, on the Metro\npath. It is what the release `.deb` ships.",
1103
+ doc: "docs/reference/navigation.md",
1104
+ heading: "Building sidebar rows",
1105
+ text: 'There are three ways to put content in the sidebar, cheapest first the\nsame ladder react-navigation\'s own `tabBarIcon` `drawerLabel` →\n`drawerContent` climbs:\n\n1. **`title` / `icon` / `color` / `count`** the convenience. Composes an\n `AdwActionRow` automatically.\n2. **`sidebarRow`** (screen option) draw one row yourself. The navigator\n keeps owning row behavior: selection, click `jumpTo`, staying in step\n with navigation state, the collapsed reveal. Return anything a\n `GtkListBoxRow` can hold React Native content, GTK widgets, a\n differently configured Adwaita row.\n3. **`sidebarContent`** (navigator prop) draw the whole pane, routing\n surface included:\n\n```tsx\n<Sidebar.Navigator\n sidebarContent={({ routes, focusedIndex, jumpTo }) => (\n <View style={{ flex: 1 }}>\n <SearchField onSubmit={filterRoutes} />\n <ScrollView style={{ flex: 1 }}>\n {routes.map((route, index) => (\n <Pressable\n key={route.key}\n onPress={() => jumpTo(route.name)}\n >\n <Text\n style={{\n padding: 8,\n fontWeight: index === focusedIndex ? "700" : "400",\n }}\n >\n {route.title}\n </Text>\n </Pressable>\n ))}\n </ScrollView>\n <StorageUsageFooter />\n </View>\n )}\n>\n <Sidebar.Screen\n name="Inbox"\n component={InboxScreen}\n />\n <Sidebar.Screen\n name="Trash"\n component={TrashScreen}\n />\n</Sidebar.Navigator>\n```\n\n`SidebarContentProps` carries `routes` (each with `key`, `name`, resolved\n`options`, resolved `title`, and `focused`), `focusedIndex`, and\n`jumpTo(name)`. `route.title` is already resolved (`options.title`, falling\nback to the route name). `jumpTo` reveals the content pane when collapsed,\nthe same as a native row click use it rather than dispatching directly,\nso selection cannot drift from navigation state. The pane\'s header bar and\n`sidebarTitle` still belong to the navigator; `sidebarContent` replaces only\nthe body under it. A sidebar built from GTK widgets instead of React\nNative content wraps its own tree in `WidgetContent`, the same escape\nhatch `contentLayout: "widget"` uses for a screen body.\n\nThe reason rungs 2 and 3 exist at all: `AdwActionRow` carries Adwaita\'s own\nrow metrics, not a default this package picked — measured at roughly\n104 px per row (with a prefix and/or count laid out) against roughly 40 px\nfor a plain title-only row and nothing passed to\n`title`/`icon`/`color`/`count` changes that height. A screen on rung 1 has\nno lever for it; a different height or density means climbing to\n`sidebarRow` or `sidebarContent`.',
1082
1106
  },
1083
1107
  {
1084
- doc: "docs/getting-started.md",
1085
- heading: "One file (Metro path)",
1086
- text: "`build-linux` produces three artifacts. Which one you want is a question\nabout the delivery channel, not about the build — they share the same\nMetro step and differ only in how much of the runtime travels with the\napp:\n\n| Flag | Artifact | Needs installed | Size (`hn-app`, linux-arm64) |\n| -------------- | -------------------------- | ---------------------------------- | ---------------------------- |\n| _(none)_ | `dist/main.jsbundle` | a `node_modules` tree **and** Node | 0.4 MB + the tree |\n| `--standalone` | `dist/<name>.cjs` | Node only (`Depends: nodejs`) | 6.9 MB |\n| `--sea` | `dist/<name>` (executable) | nothing at all | 104 MB (30 MB compressed) |\n\n```bash\nnpx react-native build-linux --standalone # in the app root\nnode ./dist/<your-package-name>.cjs # one script, system node\n\nnpx react-native build-linux --sea\n./dist/<your-package-name> # one executable, nothing else\n```\n\nBoth flags produce the jsbundle exactly as before, then one additional\nfile next to it. `--sea-output <path>` overrides where it goes; the\ndefault is `dist/<package name>` with any npm scope stripped (plus `.cjs`\nfor `--standalone`).\n\n**Pick `--standalone` for anything installed through a package manager.**\nIt is the same shape gtkx's own packaging produces and the same shape the\nvite path already ships in its `.deb` — a bundle plus a `nodejs`\ndependency — and it is the lightest of the three by any measure that\ncounts: the plain jsbundle looks smaller only because its `node_modules`\ntree is not weighed. **Pick `--sea` for \"download this one file and run\nit\"**, where nothing can be assumed to be installed. They are not\ncompeting implementations: `--sea` is `--standalone` with a copy of Node\nwrapped around it, and that copy is the entire 97 MB between them.\n\nA tagged release of this repo publishes the `--sea` executable for\n`hn-app`, `zstd`-compressed, alongside the `.deb`s (`zstd -d` it and run\nit). The `.deb`s remain how you install these apps; the executable is\nthere for the machine that has no Node to depend on.\n\nThat copy is stripped of its debug symbols as part of the build, which is\nnot a micro-optimisation: the `node` binary NodeSource distributes for\nUbuntu ships `with debug_info, not stripped` — 117 MB, 98 MB after\n`strip --strip-all` — so 19 MB of every unstripped SEA is debug\ninformation for Node's own C++, which nothing in a shipped app can use.\nThe step is best-effort: a build machine without binutils gets a warning\nand a larger executable, not a failed build. It also runs strictly before\npostject, since `--strip-all` removes exactly the kind of non-allocated\nsection the injected blob is. What remains after that is Node itself, and\nit does not compress away either — but it does compress: 30 MB with\n`zstd -19`, which is what a download actually costs.\n\nThe native addon (`@gtkx/native-*.node`, a real `dlopen`ed library) cannot\nbe JavaScript, so both artifacts carry it as bytes — a SEA asset in the\nexecutable, a base64 literal in the `.cjs` — and extract it to\n`$XDG_CACHE_HOME/react-native-gtkx-sea` on first run, keyed by content\nhash. That is what keeps \"one file\" honest in both cases.\n\nNothing extra to install to bundle it. That work is done by **rolldown**,\nwhich is vite's own engine — vite 8 depends on it outright, `@gtkx/cli`\ndepends on vite, and this package depends on `@gtkx/cli`, so it is already\nin every install. (esbuild, which gtkx's tutorial uses for the same job,\nwould have been the one genuinely new bundler in the tree: vite 8 lists it\nas an _optional_ peer and does not install it.)\n\nOne thing `--sea` does need that a plain `build-linux` does not: **the\ngtkx codegen store**, and therefore GTK development headers on the build\nmachine. A plain `build-linux` deliberately needs neither — Metro\nexternalizes every GTK module — but the SEA inlines `virtual:gtkx-config`,\nwhich re-exports `@gtkx/jsx/metadata`, a codegen product. `build-linux\n--sea` runs `gtkx codegen` itself; it just can't do so on a machine\nwithout the headers.\n\n`postject` is fetched through `npx` at build time, so the first run needs\nnetwork access.\n\nThis follows gtkx's own tutorial (`gtkx-org/gtkx examples/tutorial`:\nbundle to CJS, `node --experimental-sea-config`, postject injects the blob\ninto a copy of the `node` binary) for the SEA/postject mechanics.\nIt diverges on the two hard parts specific to this project full\nreasoning, including everything found empirically while building it (not\njust designed on paper), lives in\n`packages/react-native-gtkx/src/sea/bundle.ts` and `native-shim.ts`; the\nshort version:\n\n- **The native addon** (`@gtkx/native-<platform>-<libc>`, loaded through\n dlopen) cannot be embedded as bundled code a SEA is a V8 code cache\n blob, dlopen needs a real file. The tutorial's own answer is to keep it\n BESIDE the executable; that's two files, which is exactly what this\n build exists to stop being. This build embeds it as a Node SEA \"asset\"\n instead and extracts it to a per-user cache directory\n (`$XDG_CACHE_HOME/react-native-gtkx-sea`, falling back to `os.tmpdir()`\n for a read-only `$HOME`) on first run, keyed by a content hash so\n repeat launches reuse the extracted file. Loading it back turned out to\n need `process.dlopen()`, not `require()` — a SEA's main script can only\n `require()` built-ins and embedded assets (confirmed empirically:\n `require(anyAbsolutePath)` throws `ERR_UNKNOWN_BUILTIN_MODULE`) — and,\n found only by actually running the result, an explicit\n `nativeModule.exports.init()` call right after `dlopen()`: without it\n the first GTK-driven callback into JS panics on the Rust side (\"the\n Node environment was accessed from a thread it is not installed on\").\n- **Metro's externals** (`HOST_MODULE_EXTERNALS`) are inlined by a\n generated entry — a third host implementation alongside `host.ts` and\n `host-dev.ts` — that `await import()`s every externalized name and\n assembles `globalThis.__hostModules` before running the jsbundle text,\n instead of the app needing a runtime `node_modules` to load them from.\n `gtkx.config.ts` is resolved once, at bundle time (like the vite path\n already does), not on every process start (like `host.ts` does) — a SEA\n has no \"app root\" to read a config file from at runtime.\n\nSize, measured on the one platform this was built and proven on\n(linux-arm64): **104 MB** for `hn-app`, 30 MB compressed. Stripped Node is\n~98 MB of that the bundled app code plus the embedded native addon is\nunder 7 MB. Worth saying plainly: that is still a heavy download for what\na Hacker News reader needs, and it will not shrink further while the\nartifact carries a full Node binary. That is the trade `--sea` exists to\nmake, and `--standalone` is the answer whenever it isn't worth it.\n\n**Proof, not just a build**: copied the executable alone (no `node_modules`,\nno source tree) to an isolated directory on the VM, removed `/usr/bin/node`\nfrom the system (confirmed `command -v node` found nothing), launched the\nbinary under a headless Wayland compositor, and screenshotted a live,\nworking \"Hacker News\" window with real fetched data — not a build log, not\na run from the source tree.\n\n**vite path not done here.** Investigated, and it does not generalize\nthe same way: the vite bundle loads the native addon through\n`createRequire(import.meta.url)(\"./gtkx.node\")` a dynamically obtained\n`require`, not a literal `require(...)` call — which a bundler does not\nintercept the way it intercepts a static import (verified: the resolve\nhook never fires for it in a real rebuild of `dist/bundle.js`). The vite\nbundle also has its own top-level await, incompatible with the CJS format\na Node SEA main script requires. Both are fixable in principle (a\ntext-level rewrite of the compiled `require` call before re-bundling,\nversion-coupled to `@gtkx/cli`'s vite plugin), but that is a different,\nmore fragile technique than the Metro path's, and wasn't built or proven\nhere. If a true single file is wanted for the vite path too, that rewrite\nis where to start — not a repeat of this approach.",
1108
+ doc: "docs/reference/navigation.md",
1109
+ heading: "Grouping rows",
1110
+ text: '![The gallery sidebar, light theme: a "React Native" section header above Views, Text, Layout, Clipping and the rest, then a "gtkx" header above Widget hosting and Adwaita stack.](../shots/gallery/sidebar-groups-light.png)\n\n![The same sidebar in the dark theme, with the headers equally legible.](../shots/gallery/sidebar-groups-dark.png)\n\n_The gallery\'s own screenshots elsewhere on this site are all native\nGNOME/Adwaita chrome in the dark themethis pair is the one deliberate\nlight/dark comparison._\n\nConsecutive screens sharing a `group` value get one Adwaita section header\nabove the first of them. The header is a decoration owned by the row below\nit, not a row of its ownit sits outside the list\'s selection model and\noutside its focus chain, so arrow keys and Tab walk past it and assistive\ntechnology never announces a row that cannot be activated.\n\nGrouping follows row order: screens in one group must be declared\ntogether, and a group name reappearing after a gap starts a second header\nrather than reordering anything. Leaving `group` unset on every screen —\nthe default keeps the list flat.',
1087
1111
  },
1088
1112
  {
1089
- doc: "docs/getting-started.md",
1090
- heading: "Examples in the repository",
1091
- text: "- `examples/profile` a static layout; the same source also builds with react-native-web (`examples/profile-web`);\n- `examples/gallery` the entire v1 surface, one capability per sidebar section: views, text and layout; inputs, buttons and toggles; lists, media and SVG; Animated, interpolate and transforms; the three Reanimated sections; the three gesture sections; drag-and-drop, real upstream libraries, widget hosting, the Adwaita stack, Modal and the imperative APIs;\n- `examples/rn-app` a cli-init React Native app with ios + android + linux;\n- `examples/hn-app`a Hacker News reader on the Metro path: live API data over Node fetch, state-based two-screen navigation, a lazily loaded comment tree.",
1113
+ doc: "docs/reference/navigation.md",
1114
+ heading: "Collapsing",
1115
+ text: "Any route becoming active while collapsed reveals content\n(`AdwNavigationSplitView`'s `showContent`, a plain native property write,\nnot React state) a row click or a programmatic `navigate()`/`jumpTo()`;\nthe native back button that then appears reverses it. Re-selecting the\nsame, already-active row also reveals content again, since GTK's\n`row-selected` does not refire for a re-click with no selection change.\n\nResizing back above `collapseWidth` and then below it again does not reset\nthe selection or which pane is showing both simply persist across the\nround trip, the same size-class behavior a mobile master-detail app relies\non.",
1092
1116
  },
1093
1117
  {
1094
- doc: "docs/getting-started.md",
1095
- heading: "Tests",
1096
- text: 'Unit logic is plain vitest — no special setup, runs anywhere. Component\ntests render real GTK widgets under a headless Wayland compositor, and\nreact-native-gtkx ships the whole recipe as two subpaths so a consumer app\ndoes not have to rediscover it:\n\n- `react-native-gtkx/vitest``reactNativeGtkxTest()`, a ready Vitest\n project config: the headless-compositor plugin, the `react-native` alias\n and Metro-style platform extensions, an inline-deps default for RN\n libraries that import `react-native` themselves (`@react-navigation`),\n and the React act-environment setup;\n- `react-native-gtkx/testing` — re-exports `@gtkx/testing`\'s\n render/screen/userEvent/fireEvent surface (already RN-shaped: `getByText`\n finds a `Text`, `userEvent.click` walks up to a `Pressable`\'s gesture\n controller — no wrapper needed) plus `renderHookWithWindow`, for hooks\n that read the active window (`useWindowDimensions` and similar) —\n `renderHook` alone mounts into a windowless container.\n\nMinimal `vitest.config.ts`:\n\n```ts\nimport { reactNativeGtkxTest } from "react-native-gtkx/vitest"\nimport { defineConfig } from "vitest/config"\n\nexport default defineConfig(reactNativeGtkxTest())\n```\n\nThe default test glob is `**/*.gtk.test.{ts,tsx}`; override `include` (and\n`name`, `headless`, `platform`, `inlineDeps`, `setupFiles`,\n`fileParallelism`) through `reactNativeGtkxTest`\'s options. For a project\nthat also has portable unit tests, use the result as one entry of\n`test.projects` instead of the whole config — `vitest.config.ts` at this\nrepo\'s root is the reference (`process.platform === "linux"` guards the\ngtk project so `npm test` still works on a non-Linux dev machine, running\nonly the unit project there).\n\n```tsx\nimport { Root } from "react-native"\nimport { render, screen } from "react-native-gtkx/testing"\nimport { expect, it } from "vitest"\nimport { App } from "../src/App"\n\nit("renders the greeting", async () => {\n // react-native-gtkx components need a layout root — AppRegistry.runApplication()\n // in the real app, <Root> in a test.\n await render(\n <Root\n width={800}\n height={600}\n >\n <App />\n </Root>,\n )\n expect(screen.getByText("Hello, GNOME!")).toBeTruthy()\n})\n```\n\nRequirements: a headless Wayland compositor and D-Bus on PATH — the same\nsystem packages CI installs, `sway xwayland dbus` (Ubuntu:\n`apt install sway xwayland dbus`). A missing compositor fails a test run\nwith a readable error (`Cannot find the "sway" executable on PATH`) rather\nthan hanging. `gtkx codegen` must already have generated the project\'s\n`@gtkx/gi` bindings before the first test run — a bare `vitest run` does\nnot trigger codegen itself, unlike `gtkx dev`/`gtkx build`; the template\'s\nown `package.json` wires this as a `pretest` script.\n\n`packages/react-native-gtkx/tests/gtk/` is this repo\'s own suite, built on\nthe same `@gtkx/testing` surface directly (it tests source, not the\npublished package) — a good place to see more query and `userEvent`\npatterns in context. Query roles with `Gtk.AccessibleRole` enums (see\ndocs/gtkx-rc4-notes.md for the live workarounds still baked into that\nrecipe).',
1118
+ doc: "docs/reference/navigation.md",
1119
+ heading: "Sidebar transition events",
1120
+ text: "- **`sidebarShown`** (`{ data: undefined }`)Fires when the split\n view's own back affordance (back button, Escape, back gesture) hides the\n content pane while collapsed, returning to the sidebar.\n\nDiffers from react-navigation: this is the one case where a native,\nuser-driven interaction does get an event. Unlike a stack pop, nothing is\nremoved from `TabRouter`'s state when this happens the same route stays\nfocused, only the visible pane changes so there is no state change for\nan app to observe any other way. `sidebarShown` fires on the currently\nactive route, never for content being revealed (that direction is already\nan ordinary state change), and never at all when `collapseWidth` is unset.",
1097
1121
  },
1098
1122
  {
1099
- doc: "docs/getting-started.md",
1100
- heading: "MCP server for agents",
1101
- text: 'An agent working inside a project that depends on react-native-gtkx can\nask the library about itself instead of guessing: `react-native-gtkx-mcp`\nis a [Model Context Protocol](https://modelcontextprotocol.io) server that\nships as a `bin` on this package. Register it in `.mcp.json` (Claude\nCode, project-level) or the equivalent config of any MCP-compatible\nclient:\n\n```json\n{\n "mcpServers": {\n "react-native-gtkx": { "command": "npx", "args": ["react-native-gtkx-mcp"] }\n }\n}\n```\n\nRunning it as `npx react-native-gtkx-mcp` from the project root resolves\nthe locally installed `node_modules/.bin` entry no separate install,\nand it always answers for the exact react-native-gtkx version the\nproject actually has.\n\nThree tools:\n\n- `rn_gtkx_list_surface` browse the surface without knowing a name\n first (portable components/APIs, gtk/adw widgets, common) with counts;\n- `rn_gtkx_describe_component` the one to reach for first: does a\n component/widget exist, which subpath it is exported from, what GTK\n widget backs it, what differs from React Native, whether a gtk/adw\n widget is wrapped (takes `style`/`onLayout`) or raw;\n- `rn_gtkx_search_docs` free-text fallback for symptoms and known-issue\n questions the other two cannot answer by name.\n\nIt works without GTK installed — plain Node, no `@gtkx/*` import\nanywhere in it, reading only the package\'s own bundled docs/manifest data.\nThat matters in practice: the agent is often reading the project from a\nMac, with no GTK toolchain around at all.',
1123
+ doc: "docs/reference/navigation.md",
1124
+ heading: "Typed factories",
1125
+ text: "`createStackNavigator<ParamList>()` and `createSidebarNavigator<ParamList>()`\nare generic: the returned `Navigator`/`Screen` pair is typed against\n`ParamList`, so a mistyped screen name or a mismatched param type is caught\nat the JSX call site. Each factory has its own screen-props helper for a\ncomponent that reads `route`/`navigation` directly as props —\n`StackScreenProps<ParamList, RouteName>` for the stack navigator,\n`SidebarScreenProps<ParamList, RouteName>` for the sidebar navigator and\nits own navigation-helpers type for a component that instead reaches its\nnavigation object through `useNavigation()` (one `component` shared across\nseveral routes, for example): `useNavigation<StackNavigationHelpers>()` /\n`useNavigation<SidebarNavigationHelpers>()`.\n\nExported types: `StackNavigationOptions`, `StackNavigationEventMap`,\n`StackNavigationHelpers`, `StackScreenProps`, `StackScreenConfig`,\n`TypedStackNavigator`, `SidebarNavigationOptions`,\n`SidebarNavigationEventMap`, `SidebarNavigationHelpers`,\n`SidebarScreenProps`, `SidebarScreenConfig`, `TypedSidebarNavigator`,\n`SidebarContentProps`, `HeaderButton`.",
1102
1126
  },
1103
1127
  {
1104
- doc: "docs/getting-started.md",
1105
- heading: "Next steps",
1106
- text: "- [docs/api.md](api.md) the entire v1 surface and differences from RN;\n- [CONTRIBUTING.md](../CONTRIBUTING.md) developing the library itself (from macOS via the UTM VM);\n- [docs/gtkx-rc4-notes.md](gtkx-rc4-notes.md) — the gtkx rc.4 baseline: workarounds, what it fixed, quirks that stay.",
1128
+ doc: "docs/reference/navigation.md",
1129
+ heading: "Unsupported screen options",
1130
+ text: "react-navigation's own navigator factory is untyped upstream, so neither\nTypeScript nor the runtime otherwise says anything about a screen option\nthis adapter does not recognize (a `@react-navigation/native-stack` option\nthat does not apply here, for instance). Each navigator instead logs one\ndevelopment-only warning per unknown option key, naming the option and why\nit is ignored for example, `headerStyle`/`headerTintColor`/\n`headerTitleStyle` are ignored because Adwaita's theme owns the chrome\nstyling on this platform, `presentation` is ignored because only `\"card\"`\nexists today, and `detachInactiveScreens`/`freezeOnBlur`/`inactiveBehavior`\nare ignored because pushed pages always stay mounted, with no unmount/\nfreeze knob to offer. The warning fires once per navigator kind per key,\nnot once per screen or per render, and never in production.",
1107
1131
  },
1108
1132
  {
1109
- doc: "docs/gtkx-rc4-notes.md",
1110
- heading: "Live workarounds",
1111
- text: "| Name | What rc.4 does | Our workaround | Removal condition |\n| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| `use-signal-stale-handler` | `useSignal` still routes the handler through React's `useEffectEvent`; `react-reconciler@0.33.0` only refreshes it in `commitBeforeMutationEffects` for `case 0` (FunctionComponent)`case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through unrefreshed, so any `useEffectEvent` in a `memo`/`forwardRef` component is pinned to its mount closure forever (our `ScrollView` is a `forwardRef` with the `useSignal` calls inside it) — a fetch-fed FlatList empties itself on the first scroll. **rc.4 did not bump React and did not change the path** see below | `gtkx/bridge/use-signal.ts` re-pins the latest handler (insertion effect) and hands gtkx a stable wrapper; the bridge exports that hook, not gtkx's | A stable React 19.3 (React fixed the refresh on the 19.3 line) |\n| `runtime-dedupe` | Two bundled copies of the gtkx runtime still double-init GLib and abort; nothing guards against it. Reproduced on rc.4: `g_log_set_writer_func() called multiple times`, SIGABRT | `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits | Idempotent runtime init upstream, or an error that names the duplicate |\n| `prop-portal` | `createPortal(children, container, key?)` is byte-for-byte the rc.3 signature and can still only target a container's DEFAULT slot (\"children\"). Every other slot an object exposes declaratively a window's `Gio.ActionMap` (`actions`), a widget's `controllers`, an `AdwApplicationWindow`'s `breakpoints` is reached only by passing an element-valued PROP, which the reconciler routes through an internal `\"gtkx:prop\"` element. rc.4 exports that element from neither `@gtkx/react` nor `/internal`, and its `exports` map now refuses the deep path outright | `gtkx/bridge/slot-portal.ts` restates the `\"gtkx:prop\"` element name and wraps it in `createSlotPortal(children, target, slot)` the one line that would move if gtkx renames it. `WindowActions`/`ApplicationActions`/`WindowControllers` are built on it | gtkx exports a slot-aware portal (or the prop element) from a public entry point |\n| `renderhook-no-window` | `renderHook` still mounts into a bare `Gtk.Box` — rc.4's `render-hook.js` is byte-identical to rc.3's, which was byte-identical to rc.2'sso window-dependent APIs have no toplevel to read | Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) | `renderHook` mounts into the same harness window `render` uses |",
1133
+ doc: "docs/reference/reanimated-compat.md",
1134
+ heading: "One thread, not two",
1135
+ text: "On mobile, Reanimated exists to cross a thread boundary: JS and the UI run on\nseparate runtimes, and worklets, shared values, `runOnUI` and the Babel\nplugin all exist to move work across it. Here GTK's main loop _is_ the JS\nthread a widget call is a synchronous C call on the same stack so a\nworklet is an ordinary function, `measure()` is synchronous, and a shared\nvalue is an observable box that updates in place.\n\nThis is upstream's own behavior, not a platform-specific reinterpretation:\nreact-native-reanimated selects this same flattened implementation for\nreact-native-windows and for the web. Its non-DOM, non-native-runtime web\nbuild is the blueprint this subpath is read off, including its pure-JS\npieces (`interpolate`, `Easing`, the spring solver's config normalization),\nwhich are ported here rather than imported.\n\nThe Babel plugin is neither required nor assumed. Its output is an ordinary\nlexical closure carrying metadata properties and no injected runtime import,\nso `'worklet'` is an inert directive a worklet is directly callable whether\nor not the plugin has run. This platform never runs Babel itself (the vite\npath bundles with rolldown; the Metro path uses the app's own stock preset),\nso an app that also ships to iOS or Android keeps the plugin for those\nbuilds without conflict.\n\nDiffers from react-native-reanimated: worklet closures use live lexical\ncapture, not the Babel plugin's by-value snapshot. This is only observable\nfor a worklet that closes over a reassigned plain `let` already a bug on\nmobile so ordinary code is unaffected.\n\nDependency tracking in `useDerivedValue` and `useAnimatedReaction` is dynamic\nrather than static: a mapper subscribes to the shared values it actually\nreads on each run, rather than to a Babel-collected `__closure` list. A\n`dependencies` array is accepted and honored it still controls when a\nmapper rebuilds but it is never required for correctness, and a\nconditional read is tracked correctly either way.\n\n`makeShareableCloneRecursive` and `isWorkletFunction` are re-exported\ndirectly from the worklets subpath described at the end of this page — the\nsame instance, not a second implementation.",
1112
1136
  },
1113
1137
  {
1114
- doc: "docs/gtkx-rc4-notes.md",
1115
- heading: "`use-signal-stale-handler` is a decision upstream made, not an oversight",
1116
- text: "We shipped the fix as a PR (gtkx-org/gtkx#469) and it was **closed unmerged\non purpose**. @eugeniodepalo: _\"closing this in favour of waiting for\nupstream… Since React fixes this properly on 19.3 for all fiber tags, I'd\nrather take the version bump than carry a workaround I'd revert.\"_ So this\nrow does not move on any gtkx release only a stable React 19.3 retires it,\nand the hazard is wider than `useSignal`: any hook built on `useEffectEvent`\ninherits it until then.\n\nrc.4 was checked against that condition anyway, because the condition names a\nReact version and a release could satisfy it by bumping one: it does not.\n`@gtkx/react@1.0.0-rc.4` peers `react: ^19.2` and depends on\n`react-reconciler: ^0.33.0` character-identical to rc.3 and React's own\n`latest` is still 19.2.8, with 19.3.0 published only as canaries. rc.4 did\ntouch `useSignal`, but only to rename its options (`after`/`immediate` →\n`isAfter`/`isImmediate`); the body still calls `useEffectEvent`, and its\ndoc comment still says React fixes this on the 19.3 line.",
1138
+ doc: "docs/reference/reanimated-compat.md",
1139
+ heading: "Shared values and animations",
1140
+ text: "![The gallery's Reanimated values section: a shared value dragged and sprung back, with the render counters proving zero React renders per frame.](../shots/gallery/reanimated.png)\n\n`useSharedValue`, `makeMutable`, `isSharedValue` and `cancelAnimation` are\nfully implemented. A shared value doubles as one of the platform's own\nanimated nodes, so it can be handed straight to a `View`'s style, in addition\nto being read inside `useAnimatedStyle`.\n\nA shared value can be written either of two ways:\n\n```tsx\nsharedValue.value = x\nsharedValue.set(x) // also takes an updater: count.set((c) => c + 1)\n```\n\nBoth are real and both work; `.get()`/`.set()` is the pair upstream added for\nexactly one situation this platform inherits. The React Compiler — on by\ndefault on the vite path (see\n[the Guide's toolchains page](../guide/toolchains.md#the-react-compiler-vite-path-only)) —\ntreats anything a hook returns as frozen, so `react-hooks/immutability`\nreports every assignment to `.value`, including ones inside a callback or\neffect that are perfectly legitimate. `.get()`/`.set()` lints clean\neverywhere; `.value` keeps working, so a ported app never has to be\nrewritten.\n\nDiffers from react-native-reanimated: `SharedValue.addListener` accepts both\nupstream's `(listenerID, listener)` signature and this platform's own\nanimated-node signature, `(callback) => id`. Both call sites are real in\npractice, and supporting only one would fail the other silently.",
1117
1141
  },
1118
1142
  {
1119
- doc: "docs/gtkx-rc4-notes.md",
1120
- heading: "How each was checked against rc.4",
1121
- text: 'The rule is that a changelog entry is a claim and the removal condition is\nthe test, so each row was re-run on the real runtime rather than read about.\n\n- **`use-signal-stale-handler`** — `tests/gtk/bridge/use-signal-upstream.gtk.test.tsx`\n calls gtkx\'s own hook directly on a `memo` component; it is an `it.fails`\n guard that starts passing the day the defect is gone. On rc.4 it still\n fails, and it is the "1 expected fail" the whole suite reports.\n- **`runtime-dedupe`** — the first two attempts at a probe both said "no\n abort", and both were wrong, which is worth recording: Node caches a native\n addon by the resolved path of the `.node` FILE, so a second copy of the thin\n `@gtkx/native` JS wrapper shares one addon instance and one Rust static, and\n a second `init()` on it returns normally — on rc.3 exactly as on rc.4. The\n failure needs two DISTINCT `.node` files in one process (an app with its own\n `@gtkx/native-linux-*-gnu` plus a nested one under the library), each\n carrying its own `glib::log::WRITER_FUNC`. Built that way, rc.4 dies:\n `gtkx: GLib-ERROR: g_log_set_writer_func() called multiple times`, exit 134,\n core dumped. Neither half of the condition is metthe init is not\n idempotent, and while the error names the symbol it does not name the\n duplicate package, which is the part that would make it debuggable.\n\n **Not the only SIGABRT out of this subsystem.** A second, separate crash —\n a Rust panic inside the `writer_trampoline` `log_set_writer_func` installs\n (registered fine, once) rather than a double-registration twice took\n down a CI worker fork under `tests/gtk/dnd/collision-thresholds.gtk.test.tsx`\n (2026-08-04, runs 30903167960 and 30904467362). Not a new\n `RC4-WORKAROUND` row: the trigger was our own bug (`scroll-view.tsx`\'s\n `syncAdjustmentRange` calling `Gtk.Adjustment.configure()` with an invalid\n range, retried every frame), now fixed, not gtkx drift to absorb in the\n bridge. Full backtrace and the upstream ask (harden `writer_trampoline`\n with `catch_unwind`) are in\n [docs/upstream-gtkx.md](upstream-gtkx.md#2-a-panic-inside-the-glib-log-writer-trampoline-aborts-the-whole-process-not-just-the-offending-log-call).\n\n- **`prop-portal`** enumerated the real module objects on the runtime rather\n than reading the `.d.ts`. `@gtkx/react` exports exactly `createPortal`,\n `createRoot`, `quit`, `rootElement`, `useApplication`, `useBindSetting`,\n `useParentWindow`, `useProperty`, `useSetting`, `useSignal`;\n `@gtkx/react/internal` exports `applyWrite`, `createApplicationComponent`,\n `createElementComponent`, `createReconcilerRoot`, `createWindowComponent`,\n `getAccessibleMetadata`, `isRootElement`, `setReconcilerErrorHandler`,\n `useMergedRef`. No value in either is `"gtkx:prop"`, and there is no\n slot-aware portal. rc.4 also made the fallback worse rather than better:\n importing `@gtkx/react/dist/components/element.js` now fails with _"not\n exported under the conditions [node, development, import]"_, so restating\n the literal is the only route left. The literal itself did not move —\n `const Prop = "gtkx:prop"` is unchanged in rc.4\'s `components/element.tsx`\n (only a doc comment above it was deleted), which the passing\n `WindowActions`/`WindowControllers`/breakpoint suites confirm functionally.\n- **`renderhook-no-window`** `RenderHookOptions` still carries only\n `wrapper` and `initialProps`, no `container`, and on the runtime\n `renderHook` took the toplevel count from 0 to 0 while `render` took it\n from 0 to 1 in the same file.',
1143
+ doc: "docs/reference/reanimated-compat.md",
1144
+ heading: "`with*()` animations",
1145
+ text: "![The gallery's Reanimated motion section: the five animation functions (withTiming/withSpring/withSequence/withRepeat/withDelay) and seven easing curves compared side by side.](../shots/gallery/reanimated-motion.png)\n\n`withTiming`, `withSpring`, `withSequence`, `withRepeat` and `withDelay` are\nfully implemented for numeric values, on upstream's own defaults (timing:\n300 ms, `Easing.inOut(Easing.quad)`; spring: `GentleSpringConfig`), driven by\nthe platform's single frame scheduler. Each can be assigned directly to a\nshared value or returned from a `useAnimatedStyle`/`useAnimatedProps`\nupdater.\n\nDiffers from react-native-reanimated: re-aiming a running animation (giving\nit a new target while it is mid-flight) keeps the animation's current value\nbut takes only the new descriptor's velocityupstream also carries the\nprevious animation's velocity across the re-aim. A target that moves every\nframe ends up slightly more damped here than upstream.\n\n`withDecay` and `withClamp` are fully implemented, including `velocity`,\n`deceleration`, `velocityFactor`, `clamp` and `rubberBandEffect` — upstream's\nown step function, ported. `withDecay` is what an inertial fling rides on:\nreleased with a velocity, it coasts, decelerates, and stops with no target\nto reach. `withClamp` runs its inner animation un-truncated and only clips\nwhat reaches the value, which is observable on an overshooting spring —\nupstream's own distinction.\n\nDiffers from react-native-reanimated: `withDecay`'s config (`clamp` shape,\n`velocityFactor > 0`, `rubberBandEffect` needing a `clamp`) is validated at\nthe `withDecay()` call itself rather than on the animation's first frame —\nsame errors, one line earlier.\n\nDiffers from react-native-reanimated: the spring's rest condition is derived\ndifferently. Upstream stops a spring once its remaining energy drops below a\nfraction of its initial energy; this platform's solver stops on displacement\nand speed thresholds, derived from the same energy budget. The stopping\npoint differs by well under a pixel. A layout-animation builder's\n`.restDisplacementThreshold()` and `.restSpeedThreshold()` are accepted and\nignored for the same reason.",
1122
1146
  },
1123
1147
  {
1124
- doc: "docs/gtkx-rc4-notes.md",
1125
- heading: "What rc.4 renamed under us",
1126
- text: "rc.4 is a naming-convention sweep. Nothing below changed behaviour, but each\none is a compile error or a silent runtime miss for a consumer of the RC.\n\n- **`@gtkx/react` moved the settings types off its public entry point.**\n `SettingsSchema`, `SettingsSchemaKeys` and `SettingValue` are now exported\n from `/internal` only, while the hooks they type (`useSetting`,\n `useBindSetting`) stay public — so an app that wants to name the type of a\n setting has no supported import for it. The bridge re-exports them from\n `/internal`; the ask to put them back is in\n [docs/upstream-gtkx.md](upstream-gtkx.md). (`MenuItem` and `VflConstraints`\n left the public entry too; nothing here used them.)\n- **`@gtkx/codegen`'s `runCodegen` result renamed `regenerated` →\n `isRegenerated`** (`src/runner/index.ts`), and the package dropped its\n `./gi` and `./jsx` subpath exports in favour of a new `./internal`.\n- **`@gtkx/vitest` renamed `GtkxPluginOptions` → `PluginOptions`**\n (`src/vitest/index.ts`).\n- **The element config renamed `lazy` `isLazy` and `omitProps` →\n `omittedProps`.** This is the one with no compiler behind it: our three\n hosts synthesize `virtual:gtkx-config` as SOURCE TEXT\n (`src/runner/host.ts`, `src/runner/host-dev.ts`,\n `src/sea/gtkx-config-module.ts`), so a stale key typechecks perfectly and\n simply stops marking elements lazy at runtime. Caught by diffing rc.4's own\n `renderConfigModule` against ours and proven by the headless `run-linux`\n proof, not by a gate.\n- **`virtual:gtkx-config`'s metadata constants** went `SIGNALS` `signals`,\n `CONSTRUCT_PROPS` `constructProps`, `CONSTRUCT_ONLY_PROPS` →\n `constructOnlyProps`, `DEFAULT_PROPS` `defaultProps`. Free for us: all\n three hosts re-export the module wholesale\n (`export * from \"@gtkx/jsx/metadata\"`) rather than naming its members.\n- **`@gtkx/testing` renamed `GtkxElementError` `ElementError`,\n `render`'s `animations`/`reactStrictMode` `areAnimationsEnabled`/\n `isReactStrictMode`, and `prettyWidget`'s `highlight` → `shouldHighlight`.**\n We use none of them, but a consumer's test suite will.\n- **`defineBehavior`'s `createContext` → `initialize`**, and\n `@gtkx/utils` dropped its `./function` subpath. Neither reaches us.",
1148
+ doc: "docs/reference/reanimated-compat.md",
1149
+ heading: "Spring presets",
1150
+ text: "Eight named configs ship as plain data, mirrored exactly from upstream:\n\n| Preset | Values |\n| -------------------------------------------- | ---------------------------------------------------------------- |\n| `Reanimated3DefaultSpringConfig` | `damping: 10, mass: 1, stiffness: 100` |\n| `Reanimated3DefaultSpringConfigWithDuration` | `duration: 1333, dampingRatio: 0.5` |\n| `WigglySpringConfig` | `damping: 90, mass: 4, stiffness: 900` |\n| `WigglySpringConfigWithDuration` | `duration: 550, dampingRatio: 0.75` |\n| `GentleSpringConfig` | `damping: 120, mass: 4, stiffness: 900` `withSpring`'s default |\n| `GentleSpringConfigWithDuration` | `duration: 550, dampingRatio: 1` |\n| `SnappySpringConfig` | `damping: 110, mass: 4, stiffness: 900, overshootClamping: true` |\n| `SnappySpringConfigWithDuration` | `duration: 550, dampingRatio: 0.92, overshootClamping: true` |",
1127
1151
  },
1128
1152
  {
1129
- doc: "docs/gtkx-rc4-notes.md",
1130
- heading: "Fixed in rc.3 (history, one line each)",
1131
- text: '- **`gsk-colorstop-boxed-write`** constructing a `Gsk.ColorStop` threw in\n the native addon, so SVG gradients had zero constructible stops and painted\n nothing. **Fixed upstream by us** (gtkx-org/gtkx#473, closing #472): a\n record field write converts through `toNative` now.\n- **`graphene-rect-nested-boxed-props`** the same native bug reached through\n `new Graphene.Rect({ origin, size })`; same upstream fix, so `svg-node.ts`\n uses the plain constructor again.\n- **The codegen freshness lie**rc.2\'s `@gtkx/cli` could report "bindings up\n to date" over a store `npm install` had pruned. Fixed upstream in\n gtkx-org/gtkx#470 (also ours); separately `src/runner` calls the\n programmatic `@gtkx/codegen` API rather than the CLI.',
1153
+ doc: "docs/reference/reanimated-compat.md",
1154
+ heading: "Animating an object or array, not just a number",
1155
+ text: "An animated value can be a plain object or array whose leaves are numbers —\nupstream's real `AnimatableValue`, minus color strings (see below).\n`withTiming({ x: 10, y: 20 })` and `withSpring` interpolate every leaf on the\nsame curve and the same clock: a nested object recurses, and an array's own\nelements are always numbers, never nested — upstream's own asymmetry. The\ncompletion callback fires once per animation, not once per leaf, and\ncomposing through `withDelay`/`withSequence`/`withRepeat` carries a shape\nexactly as it carries a plain number.\n\nDiffers from react-native-reanimated: a target whose shape does not match\nthe value it is animating from throws, naming the mismatched leaf, rather\nthan silently dropping the key the way upstream's from-value-driven walk\ndoes. A key that previously held a plain number seeds an animation from that\nnumber, unchanged from upstream; a key that previously held a plain object\ndoes not upstream's own `prepareAnimation` has no branch for a plain data\nobject either, so it is seeded at the target exactly like a key that was\nabsent. An `{x, y}` `withTiming` costs about twice a single number's own\nper-frame cost, measured before the result ever reaches a style property.",
1132
1156
  },
1133
1157
  {
1134
- doc: "docs/gtkx-rc4-notes.md",
1135
- heading: "Fixed in rc.2 (history, one line each)",
1136
- text: "- **`vitest-compositor`** rc.1 defaulted the headless display to weston;\n rc.2's default IS sway, so `vitest.config.ts` calls the plugin with no\n arguments.\n- **`no-virtual-seat`** rc.1 had no input seat under sway, so windows never\n activated and `userEvent` was impossible; rc.2 starts a virtual seat.\n- **`fixed-layout-child`** — rc.1's declarative `<GtkFixedLayoutChild>`\n created a detached object; moot since containers moved to our own\n `RnGtkxLayout` manager and GtkFixed left the codebase.\n- **`controllers-as-children`** — rc.1 silently ignored controllers passed as\n JSX; rc.2 has a `controllers` slot on `GtkWidget`. Pressable and TextInput\n still attach theirs imperatively on purpose a choice now, not a\n workaround.",
1158
+ doc: "docs/reference/reanimated-compat.md",
1159
+ heading: "`useDerivedValue` and `useAnimatedReaction`",
1160
+ text: "Both are fully implemented, along with the `startMapper`/`stopMapper`\nprimitive they are built ona few libraries reach for that primitive\ndirectly. Mappers are torn down on unmount. `inputs`, the static candidate\nlist the Babel plugin would otherwise produce, is accepted and ignored:\ntracking is dynamic, so a mapper subscribes to what it actually reads rather\nthan to what it was told to expect.",
1137
1161
  },
1138
1162
  {
1139
- doc: "docs/gtkx-rc4-notes.md",
1140
- heading: "Behaviour rc.4 changed under us",
1141
- text: "Nothing measurable. The suite is **166 files, 1601 passed + 1 expected fail**\non rc.4 identical to main's own CI run on rc.3 (251c353), file for file and\ntest for test. The renames above are the whole of the release as far as this\nrepo can observe it: the reconciler's commit-time signal handling, the\nharness window, the accessibility tree and the codegen output all behave as\nthey did on rc.3, and the regenerated bindings typecheck clean.\n\nTwo things worth knowing before debugging something odd on rc.4:\n\n- **A first codegen after a version bump is slow enough to look like a hang.**\n The store fingerprint includes the app's own config, so each example\n regenerates once on top of the root's run~45 s for the gallery on the\n VM. `scripts/gtkx-dev-headless.ts` sleeps 25 s before its first shot and\n will report `FAST-REFRESH-FAIL` on a cold store; run `npx gtkx codegen` in\n the example first.\n- **`gtkx dev` still binds vite's HMR websocket on the fixed port 24678**, and\n the CLI exposes no way to move it. A second `gtkx dev` anywhere on the\n machine logs `WebSocket server error: Port 24678 is already in use` and the\n edit never reaches the app, while the supervisor still prints \"Fast Refresh\n complete\"so the log marker alone is not proof the refresh applied.\n Verified identical on rc.3, so this is not new, but it makes the dev-path\n proof unreliable when another app is running.",
1163
+ doc: "docs/reference/reanimated-compat.md",
1164
+ heading: "`useAnimatedStyle` and `useAnimatedProps`",
1165
+ text: "![The gallery's Reanimated limits section: a driven width next to a refused one, showing exactly where useAnimatedStyle stops driving a layout property at frame rate.](../shots/gallery/reanimated-limits.png)\n\nWhat this platform can write to a mounted widget without a React render is a\nfixed set of properties — the honest boundary of the surface, not a\ntemporary limit:\n\n| Property | Reached through | How it reaches GTK |\n| ------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `opacity` | `useAnimatedStyle` | `gtk_widget_set_opacity`, straight from the animation frame. |\n| `transform` (`translateX/Y`, `scale`, `scaleX/Y`, `rotate`/`rotateZ`) | `useAnimatedStyle` | The rect store plus one queued allocation, applied as a `GskTransform`. No 3D, no skew, no `matrix` the same list the static `transform` style takes. |\n| `top`, `left`, `right`, `bottom` — only on a node whose own `position` is `\"absolute\"` | `useAnimatedStyle` | Turned into a translation from the position the committed layout gave it — the same rect store, the same queued allocation. |\n| `width`, `height` — only where the change is confined to the node that owns it | `useAnimatedStyle` | The node's own subtree is re-laid-out pinned to the driven value, into the rect store as an override. |\n| `backgroundColor`, `color`, `borderColor` (and per side), `outlineColor` | `useAnimatedStyle` | A `GtkCssProvider` private to that widget, reloaded in place. |\n| The numeric SVG props (`r`, `cx`, `strokeWidth`, `strokeDashoffset` and the rest of the geometry and paint numbers) | `useAnimatedProps` | The shape's own descriptor plus `queueDraw` the SVG components already subscribe to an animated node themselves. |\n\nColors deliberately do not go through the memoized class registry the static\nstyles use. That registry keys on generated CSS text, so a color driven\nthrough it would mint a class per animation frame into one process-wide\nstylesheet that GTK re-parses whole and never prunes. The private provider\nhas no cache and no document, so nothing about the static path changes.\nEvery animated component gets this, not only `Animated.View` the write\npath is a hook over \"a widget and its parent\", so `Animated.Text` and\nanything through `createAnimatedComponent` animate colors on the same terms.\n\nDiffers from react-native-reanimated: the remaining layout properties —\n`flex`, `flexBasis`, every `margin*`/`padding*`, `gap`, the `min*`/`max*`\nfamily — are refused rather than driven at frame rate. Each needs a Yoga\npass plus the commit walk that follows it, and that cost scales with the\n_container_ rather than with the animated value, while a transform or a\ncolor's cost stays flat regardless of tree size. A `useAnimatedStyle` that\nchanges one of these warns once for that property, names it as a layout\nproperty, and names the transform to use instead. The value is not dropped:\nit is applied on the next React render, and when the value comes from an\nanimation the updater returned (`height: withTiming(320)`), that render is\nproduced automatically when the animation reaches its target, and at most\nonce every 100 ms while it is on its way. That is at most ten renders a\nsecond, never one per frame.\n\n`scaleX`/`scaleY` are an approximation for `width`/`height`, not a\nreplacement, and the warning for a refused size says so. A scale grows\naround the view's center, so the box moves as it grows, where a real width\nchange would not move it; and it scales the box's _content_ with it instead\nof re-laying it out, so wrapped text keeps its old line breaks and is drawn\nstretched rather than re-wrapped. Reach for a scale when the content can\ntolerate being stretched — a plain box, an image. `translateX`/`translateY`\nfor insets are exact and carry no such caveat.",
1142
1166
  },
1143
1167
  {
1144
- doc: "docs/gtkx-rc4-notes.md",
1145
- heading: "Behaviour rc.3 changed under us (still true)",
1146
- text: "- **Blockable signals are no longer suppressed for a whole React commit** —\n rc.3 wraps each framework write individually, so an emission the framework\n did not cause (one raised from a `useLayoutEffect`, or aimed at another\n `createRoot` tree) reaches its handler. Our navigators lean on this.\n- **`render`'s harness window is undecorated**, so role queries see only what\n the test rendered.\n- **A widget with `accessibleLabelledBy` reports the relation as its\n accessible name**, ahead of its own text the precedence ARIA defines.\n- **`toHaveTextContent` no longer falls back to the accessible name**;\n **`toHaveDisplayValue` throws** on a widget without one; **checked state is\n tri-state**.\n- **Records are constructible only when their bytes can be copied.** Neither\n `Gsk.ColorStop` nor `Graphene.Rect` is caught by it.\n- **Single-child widgets have no `content`/`child` props** (pass the widget as\n a child instead).",
1168
+ doc: "docs/reference/reanimated-compat.md",
1169
+ heading: "The first exception: insets on an absolutely positioned node",
1170
+ text: '`top`, `left`, `right` and `bottom` are driven at frame rate on a node whose\nown `position` is `"absolute"`. Such a node is out of flow, so moving it\nchanges nothing but where it is drawn — which makes an inset exactly a\ntranslation from the position the committed layout gave it, and lets it run\non the transform path with no Yoga pass at all. This is the shape the whole\nsortable-list ecosystem is built on:\n\n```tsx\nconst style = useAnimatedStyle(() => ({\n position: "absolute",\n left: 0,\n right: 0,\n top: top.value, // driven, with no Yoga pass\n}))\n```\n\nA few things follow from that:\n\n- **It composes with your own transform** rather than replacing it. The\n derived translation is applied outermost, so it moves the\n already-rotated, already-scaled box by the distance the layout asked for\n — a `top: 100` under `scale: 2` moves the box 100 px, not 200.\n- **`right` and `bottom` invert**, because they measure inward from the far\n edge: a larger value moves the node toward the origin.\n- **An axis anchored by both edges is still refused**, because it is no\n longer a translation. `left: 0, right: 0` with no `width` derives the\n width from both edges, so animating `left` there resizes the node; with a\n definite `width`, Yoga honors `left` and ignores `right` entirely, so\n animating `right` would invent motion a real layout pass would not\n produce. Both cases warn in their own words and name a working\n configuration.\n- **`measure()` reports the committed layout, not the translated\n position** see [Gesture and scroll integration](#gesture-and-scroll-integration).\n- **`position` may live in a sibling style entry**, as in\n `style={[styles.row, useAnimatedStyle(() => ({ top: y.value }))]}` the\n decision is made against the flattened style, not against the updater\'s\n object alone.',
1147
1171
  },
1148
1172
  {
1149
- doc: "docs/gtkx-rc4-notes.md",
1150
- heading: "Non-workarounds (quirks that stay)",
1151
- text: '- 64-bit FFI values arrive as BigInt `toNumber()` at the boundary\n (`gtkx/bridge/measure.ts`);\n- signal names are kebab-case ("value-changed"); signals do not pass the\n emitter (get the widget from a ref);\n- role queries in tests use the `Gtk.AccessibleRole` enum, not strings;\n- `npm install` prunes the codegen store (`node_modules/.gtkx` is not in the\n lockfile) run `npm run codegen` after installingnpm behavior, not gtkx;\n- measuring unmapped widgets yields 0 (offscreen Label probes are the\n exception) re-measure on the `map` signal + re-commit measured leaves on\n every flush (`layout/node.ts`);\n- mixed-session setups only: running an app on a bare compositor (headless\n sway) while `XDG_RUNTIME_DIR` points at a full GNOME session can segfault in\n a GTK signal handler when the GNOME settings portal pushes updates into the\n app (`g_cclosure_marshal_VOID__OBJECTv` via the FFI emit path); cutting\n `DBUS_SESSION_BUS_ADDRESS` avoids it, which is why the headless scripts do.\n Normal desktop and container runs are unaffected. The portal-push crash\n needs a live settings change to trigger and stays on the list unconfirmed.',
1173
+ doc: "docs/reference/reanimated-compat.md",
1174
+ heading: "The second exception: a size confined to the node that owns it",
1175
+ text: "`width` and `height` are driven at frame rate where the change stops at the\nnode: the node's own subtree is re-laid-out pinned to the driven value, the\nresult goes into the rect store as an override, and one queued allocation\nputs it on screen. Nothing above the node is visited and nothing is written\ninto Yoga, so the cost tracks the size of the node rather than of its\ncontainer.\n\n```tsx\n// A progress bar, a disclosure panel, a sliding drawer — all the same shape.\nconst style = useAnimatedStyle(() => ({ width: width.value }))\n\n<View style={{ width: 400, height: 700 }}>\n {/* the container's width is its own, so nothing this box does can move it */}\n <Animated.View style={[{ height: 60 }, style]}>\n <Text>re-wraps as the box grows, which a scaleX does not</Text>\n </Animated.View>\n <View style={{ height: 20 }} />\n</View>\n```\n\nThis is a real layout, not a stretch: the content inside is re-laid-out at\nthe new size, text re-wraps, a flex row inside redistributes, a stretched\nchild follows — which is the difference from `scaleX` and the reason this is\na Yoga pass at all.\n\nThe precondition is measured, and the refusal applies wherever it does not\nhold:\n\n- The axis is the container's **cross** axis — a `width` in a column, a\n `height` in a row. A main-axis size pushes every following sibling along,\n which is the layout pass the refusal exists to avoid.\n- The container's size on that axis does not come from its children — a\n definite or percentage size, a `flex` from its own parent, or `stretch` on\n its parent's cross axis. A content-sized container would grow with the\n node.\n- The node's other axis does not come from its content a box with\n `height: auto` around wrapping text gets taller as it gets narrower, and\n everything after it moves.\n- The node's resolved cross-axis alignment is `flex-start` or `stretch` —\n `center` and `flex-end` move the node's own origin as it grows.\n- No `aspectRatio` and no `min`/`max` on that axis — the first ties the\n other axis to this one, and the second clamps the driven value, so the\n box silently stops following the animation.\n- The container does not wrap, which would resize the node's line and move\n every line after it.\n- An absolutely positioned node qualifies on either axis, as long as that\n axis' start edge (`left`, `top`) is anchored — it then grows from an\n origin that does not move, and being out of flow, it touches nothing at\n all. This does not apply under an `IntrinsicRoot`, which reports its Yoga\n content size to GTK as the window's own size request a size below it\n deliberately never goes into Yoga, so the island would keep its old\n request while the node draws outside it.\n\nThree more properties of this path are worth knowing:\n\n- The container's `flexDirection` and `alignItems` are usually not present\n in the updater's own object — `style={[styles.bar, useAnimatedStyle(() => ({\nwidth: w.value }))]}` is the ordinary spelling, and the decision is taken\n against the real layout tree either way.\n- The driven size survives an unrelated engine flush: it is kept as an\n override next to the animated offset rather than written over the\n committed rect, so a window resize or any other reason the tree\n re-commits mid-animation — cannot drop a frame of it.\n- `measure()` reports the committed layout, not the driven size, exactly as\n it does for a transform or an animated inset — see below.",
1152
1176
  },
1153
1177
  {
1154
- doc: "docs/gtkx-rc4-notes.md",
1155
- heading: "Procedure when the next release ships",
1156
- text: "1. Update the `@gtkx/*` pins (root, spike, examples, template), then\n `npm install && rm -rf node_modules/.gtkx && npm run codegen`;\n2. Run everything on Linux: `npm run typecheck && npm test`, `build:dist`,\n `check:package`, plus the headless example proofs;\n3. Walk the live-workaround table: for each row check the removal condition,\n delete the tag and the row together when it is met, and move the entry into\n the history section above — **with a probe that proves the fix on the real\n runtime**, not just the release notes claiming it. And make the probe\n reproduce the ORIGINAL failure first: two of the three `runtime-dedupe`\n probes written for rc.4 reported a fix that was not there, because they\n were not actually building the duplicate;\n4. Re-tag whatever survives (`RC4-WORKAROUND` the new release), rename this\n file to match the new pin, and update `docs/upstream-gtkx.md` if an ask was\n answered.",
1178
+ doc: "docs/reference/reanimated-compat.md",
1179
+ heading: "`measure()` on a node moved this way",
1180
+ text: "`measure()` on a node whose position or size is being driven reports the\n**layout** rect, not the paint position: the node's Yoga `top` (or `width`/\n`height`) did not change, only its allocated and painted position did. So\n`x`/`y`/`width`/`height` are the committed layout, untranslated, while\n`pageX`/`pageY` follow GTK's real transform chain and report where the node\nis actually drawn. `measureInWindow` and `measureLayout` follow `pageX`/\n`pageY`. This is the same split an explicit `translateY` has always\nproduced here.",
1157
1181
  },
1158
1182
  {
1159
- doc: "docs/research/navigation-extensibility.md",
1160
- heading: "1. The two layers",
1161
- text: "```\nyour app\n ├── react-native portable components\n ├── react-native-gtkx/navigation react-navigation adapter (optional)\n └── react-native-gtkx/adwaita GTK widgets and primitives\n```\n\n**`react-native-gtkx/gtk` and `react-native-gtkx/adw`** owns the widget: diffing a requested stack\nof tags into `pushByTag` / `popToTag` / `replaceWithTags`, holding a popped\npage alive until its exit animation ends, bracketing transitions, reporting\nnative pops. It imports nothing from `@react-navigation/*`. `NavigationStack`\ntakes the visible stack as a prop, so a `useState` is a complete router.\n\n**`react-native-gtkx/navigation`** is an adapter: react-navigation state to\nan array of tags, a native pop to `StackActions.pop` (only when the tag is\nstill in state, otherwise it would double-pop), descriptors to titles, header\ncontent and `canPop`, plus dev warnings for options we ignore.\n\nThis is the same split the React Native ecosystem already uses:\n`react-native-screens` exposes primitives, `@react-navigation/native-stack`\nbinds them to a router. It is also what React Navigation's maintainer\nrecommended when he saw the project (u/satya164, on the r/reactnative\nannouncement): _keep your own navigator so you can provide options specific\nto GTK, unless you plan to match native stack API 1:1._\n\nThe consequence that matters: **the ceiling of react-navigation's model is\nnow only in the adapter, never in the primitive.** A GTK capability with no\ncounterpart in React Native does not have to be squeezed into someone\nelse's abstraction — it lives in the primitive layer and is reachable\ndirectly. See [../platform-layer.md](../platform-layer.md).",
1183
+ doc: "docs/reference/reanimated-compat.md",
1184
+ heading: "`zIndex`",
1185
+ text: '`zIndex` is driven, animated or not, and costs what `opacity` costs one\nwidget write, no Yoga pass, no CSS. The shape a sortable list produces every\nframe (`{ position: "absolute", left: 0, right: 0, top: top.value, zIndex:\nmoving ? 1 : 0 }`) drives both `top` and `zIndex` and warns about neither.',
1162
1186
  },
1163
1187
  {
1164
- doc: "docs/research/navigation-extensibility.md",
1165
- heading: "2. What an app can reach today",
1166
- text: "Everything below the HeaderBar: each page hosts a full RN tree in its own\nlayout root. All of react-navigation's state mechanics: params,\n`setOptions`, dispatch, resets.\n\nStack options: `title`, `headerShown`, `headerButtons` (declarative native\nicon buttons), `headerLeft` / `headerRight` (ordinary RN content rendered\n_inside_ the HeaderBar), `gestureEnabled`.\n\nPast the options, the primitives: any GTK widget we bind, taking `style` so\nReact Native drives its position and its appearance, plus `wrapReactNative`\nfor widgets we do not re-export, plus a `ref` to the underlying\n`Adw.NavigationView`. There is no wall a missing convenience costs a line,\nnot a fork.\n\n**Resolved since the first snapshot.** Kept here because the reasons are\nstill instructive:\n\n- _RN content could not size a chrome slot_ (HeaderBar start/end, sidebar\n rows) one root cause behind the whole `headerLeft`/`headerRight` class.\n Fixed by the intrinsic-size root, now public as `IntrinsicContent`.\n- _`usePreventRemove` / `beforeRemove` desynced_, because the native pop had\n already happened when state heard about it. Fixed through\n `AdwNavigationPage:can-pop`: a prevented route cannot be popped by the\n user at all, so there is nothing to race. Covered by\n `tests/gtk/navigation/prevent-remove.gtk.test.tsx`.\n- _Unsupported options were ignored silently._ Fixed:\n `src/navigation/option-warnings.ts` names the screen and the option in\n development.\n- _Screen props and options had to be hand-rolled._ Fixed:\n `createStackNavigator<ParamList>()` types `Stack.Screen`, its options and\n the screen props (`examples/hn-app` relies on it).\n\n`createSidebarNavigator`'s own gaps sidebar row rendering, collapsed\nmode and the static content header — are covered in §3 below, alongside\nthe `examples/tasks-app`/`examples/tasks-nav` narrative that found and\nthen closed them.\n\nOn typing, one clarification worth recording, since it was raised publicly.\nThe complaint was never that custom navigators cannot be typed — the docs\nshow how, and we follow them. It is that the upstream v7 signature is\n`createNavigatorFactory(Navigator: ComponentType<any>): (config?: any) => any`,\nso nothing flows out of the factory itself and the types have to come from\nannotating the navigator. React Navigation 8 replaces this with a real typed\nAPI (`NavigatorTypeBagBase`, `createScreenFactory`); adopting it is the\n`react-navigation-8` epic.",
1188
+ doc: "docs/reference/reanimated-compat.md",
1189
+ heading: "Everything else in the style",
1190
+ text: "Borders, radii and shadows still reach GTK as a CSS class computed during\nrender; a `useAnimatedStyle` that changes one of them names it once in a\nwarning and applies its latest value on the next React render produced\nautomatically when the value comes from an animation, exactly as for a\nrefused layout property. `useAnimatedProps` follows the same rule with the\nsame warning: a numeric prop is driven, anything else is named and lands on\nthe next render.",
1167
1191
  },
1168
1192
  {
1169
- doc: "docs/research/navigation-extensibility.md",
1170
- heading: "3. Still open",
1171
- text: "Meaningful on this platform and not done yet: toolbar top-bar style (the\n`headerTransparent`/`headerShadowVisible` analogue), search-bar options\n(`Gtk.SearchBar` / `headerSearchBarOptions` — note v8 renamed its\n`onChangeText` to `onChange`), and deep links (they parse, but nothing\ndelivers a URL on the desktop yet). `animation: \"none\"` is done (a screen\noption, see docs/api.md).\n\n**Resolved by building `examples/tasks-app` (the gtkx tutorial's Tasks app,\nported), each with a small library change, not a workaround:**\n\n- _`Adw.Dialog` presentation_ — confirmed working. `AdwAboutDialog`/\n `AdwAlertDialog`/`AdwPreferencesDialog`/`AdwShortcutsDialog` are already\n `wrapReactNative`-wrapped; mounted with no Yoga ancestor anywhere in the\n tree (this app has none — see the example's README), they hit\n `wrapReactNative`'s \"bare\" branch and present correctly, verified live\n with real screenshots (Preferences, Shortcuts). Nothing to fix here —\n this item can be dropped from \"still open\" entirely.\n- _Breakpoints_ — a real `Adw.Breakpoint`, verified live collapsing the\n window at a narrow width, but not through the navigator: through a new\n `AppRegistry.runApplication({ breakpoints })` parameter instead (the\n navigator itself still had no collapsed-mode concept at the time —\n closed by `navigation-depth-2`, see below). Also found and recorded:\n `AdwBreakpoint`'s `onApply`/`onUnapply` never fire under the\n `@gtkx/vitest` headless-sway gtk test project, in any form tried (JSX\n prop, imperative `Adw.Breakpoint`+`addBreakpoint`, a genuine `swaymsg`\n resize) — but fire immediately in a real GNOME session. Treat it as\n untestable headless today, not broken; see\n `packages/react-native-gtkx/tests/gtk/bridge/auxiliary-elements.gtk.test.tsx`.\n (`navigation-depth-2`'s own `collapseWidth`, below, sidesteps this\n entirely — it drives `Adw.Breakpoint.addSetter` rather than\n `onApply`/`onUnapply`, and that IS testable headless, see\n `tests/gtk/adw/breakpoint.gtk.test.tsx`.)\n- _Actions and menus_ were never on this list by name, but turned out to\n be the same kind of gap: `AppRegistry.runApplication` had no way to\n attach a `GSimpleAction`, `actionAccels` or a `GtkShortcutController` to\n the app/window it builds required for a `Gio.Notification` action\n button to route anywhere at all. Closed the same way, with\n `applicationActions`/`actionAccels`/`windowActions`/`windowControllers`.\n\n**Resolved by building `examples/tasks-nav` (`navigation-depth-2` epic),\nclosing exactly what the tasks-app port above found still narrow:**\n\n- _Sidebar row rendering and collapsed mode_ — `createSidebarNavigator`'s\n `SidebarNavigationOptions` was `{ title }` only: no per-row icon/color/\n count, and no collapsed/breakpoint wiring of its own (tasks-app had to\n reach `AppRegistry`'s `breakpoints` directly and drive `collapsed`\n itself). Fixed: `icon`/`color`/`count` (rendered as `AdwActionRow`, the\n same widget tasks-app's own hand-rolled sidebar used) and an opt-in\n `collapseWidth` prop, driving collapse through the navigator itself via\n a native `Adw.Breakpoint` — not a `useWindowDimensions` conditional; see\n [../platform-layer.md](../platform-layer.md), \"Two ways to react to\n size\", for the mechanism and why no `useBreakpoint` hook exists.\n- _One static content header shared by the whole navigator_ — the same\n port's other finding: a filter toggle group vs. a back button,\n depending on selection, did not fit one static header. The\n `navigation-depth-2` PRD explicitly allowed this turning out to be a\n structural gap; it wasn't descriptor options already merge\n navigator-level `screenOptions` with a screen's own `options` and\n re-resolve on `navigation.setOptions()`, core react-navigation behavior.\n `SidebarNavigationOptions` gained `headerLeft`/`headerRight`/\n `headerTitle`, mirroring the stack navigator's own `headerLeft`/\n `headerRight`; a screen that toggles local state and calls\n `setOptions` in an effect gets a header that changes shape with its own\n selection, no stack involved — confirming tasks-app's own conclusion\n that a stack was never the right tool for the \"open an item\" case.\n Caveat found while testing this: `setOptions` merges into the\n previously resolved options rather than replacing them (see\n docs/api.md).\n\n`examples/tasks-nav` is the same navigational shape as `examples/tasks-app`\n— smart views, colored user lists, an open-item editor — now written\nthrough `createSidebarNavigator` instead of directly on\n`AdwNavigationSplitView`/`AdwActionRow`.\n\n**Resolved by `collapse-nav` (a live bug report on `examples/tasks-nav`),\none property lower than `collapseWidth` itself:** `collapseWidth` flips\n`AdwNavigationSplitView.collapsed` correctly, but `showContent` — WHICH\npane is visible while collapsed — was only half-wired: a row click already\nrevealed content, but nothing observed the split view's own back\naffordance putting it back, and a plain programmatic `navigate()` (no row\nclick) did not reveal content at all. On read, this looked like it might\nbe the same \"the breakpoint effect sets only `collapsed`\" gap all over\nagain; it mostly was not — see `sidebar.tsx`'s own file header for what was\nalready there. Three questions were settled empirically, with a throwaway\nGTK test written BEFORE any implementation code, rather than assumed from\nlibadwaita's docs:\n\n- _Does a cold-started, already-collapsed window default to content or the\n sidebar?_ Sidebar — `showContent` defaults to `false`, confirmed by\n mounting a window already narrower than `collapseWidth` and reading the\n property on first layout, before any code (ours or the app's) ever wrote\n to it. No fix needed.\n- _Does resizing back above `collapseWidth` and back below it need to\n reset `showContent` or the selection?_ No — both persist across the\n round trip, confirmed the same way (resize wide, resize narrow again,\n read the property). This is deliberate, not an oversight: it is the same\n size-class persistence a mobile master-detail app relies on (open an\n item, rotate to landscape and back, still on that item), which is\n exactly the \"the way a mobile app does\" behavior the bug report asked\n for. Resetting it would have fought the platform's own default for no\n benefit.\n- _Does an app need to observe or control the collapsed pane at all?_ One\n direction, yes: going back. TabRouter's `state` never changes when the\n user backs out of collapsed content (nothing is removed, the same route\n stays focused), so there is no existing react-navigation mechanism for\n an app to notice it happened — unlike a stack pop, which state itself\n already reveals through the route array shrinking. A new event,\n `sidebarShown` (`SidebarNavigationEventMap`, the same `navigation.emit`/\n `addListener` protocol `StackNavigationEventMap`'s `transitionStart`/\n `transitionEnd` already established — not a second protocol), fires on\n the active route for exactly this. The forward direction (content being\n revealed) got no event: it is already an ordinary state change an app\n can observe the normal way, so an event there would be pure duplication.\n\nThe echo risk this raises — state → widget and widget state both touch\nthe same property, could they retrigger each other? — resolved the same\nway the stack navigator's own doc warns about it: by a value asymmetry, not\na flag. State → widget only ever WRITES `true`; widget → state only ever\nREACTS to `false`. Two disjoint values, so neither side can mistake the\nother's write for the other direction.\n\nFixed: `sidebar.tsx`'s `state.index` effect now also calls\n`showContentIfCollapsed()` (previously only `onRowActivated` did, so a\nclick worked but a programmatic navigation left the user stranded on the\nsidebar exactly like the report — a real, reproducible gap, not merely a\ntheoretical one); `onNotifyShowContent` is observed and re-emitted as\n`sidebarShown`. `examples/gallery` (no `collapseWidth`) is untouched by\nconstruction — every changed path checks `getCollapsed()` /\n`collapseWidth !== undefined` live first. See\n`tests/gtk/navigation/sidebar-collapse.gtk.test.tsx` for the automated\nversion of all four findings above, and docs/api.md for the public shape.\n\n**Found while building `examples/tasks-nav`, narrower, still open:**\n\n- _The sidebar PANE's own chrome has no customization hook_ — its\n `AdwToolbarView`'s `AdwHeaderBar` is hard-coded\n (`src/navigation/sidebar.tsx`); a navigator consumer can set\n `sidebarTitle` (a string) on it and nothing else. `examples/tasks-nav`'s\n \"New List\" action wanted to live there (matching tasks-app's own\n `SidebarHeader` component) but had to go on the content header instead,\n via the navigator-level `headerButtons` prop. Not on the PRD's\n checklist, so not built.\n- _Toasts_ — no `AdwToastOverlay`/`Adw.Toast` convenience exists anywhere\n in `react-native-gtkx` (upstream's own tutorial reaches for\n `@gtkx/components/adw`'s `ToastProvider`/`useToast`, a package this repo\n does not depend on). `examples/tasks-app/src/toast.tsx` is a local\n stand-in; the toast's underlying state change works and is verified live,\n but the toast's own visual appearance could not be confirmed on screen\n in that session, for a reason not yet root-caused. Worth a real fix (or\n at least a live confirmation) before another app leans on it.\n\n**Meaningless on desktop, skip forever:** status-bar and home-indicator\noptions, large titles, blur effects, gesture direction, form sheets,\nback-button labels. `headerBackButtonMenuEnabled` is free — libadwaita's\nback button already shows a history menu.",
1193
+ doc: "docs/reference/reanimated-compat.md",
1194
+ heading: "An animation returned from the updater",
1195
+ text: "`useAnimatedStyle(() => ({ height: withSpring(open.value ? 320 : 0) }))` is\nhow Reanimated's own documentation writes an animation, and it runs here on\nthe platform's one frame scheduler. Three rules — all upstream's, read out\nof its own `styleUpdater`/`prepareAnimation`, not inferred decide what a\ngiven mapper run does with it:\n\n- A key animating for the first time is seeded at its target, not animated\n to it — there is nothing to animate from.\n- A key whose previous updater result held a plain number animates from\n that number, so the common \"snap shut, open smoothly\" shape works:\n `useAnimatedStyle(() => ({ height: open.value ? withTiming(200) : 100 }))`\n and `useAnimatedStyle(() => ({ opacity: visible.value ? withTiming(1) : 0\n}))` both run their full range over the animation's duration rather than\n jumping.\n- A later run producing the same animation does not restart it compared\n by target and shape rather than by object identity, since a mapper\n re-runs many times a second and every run builds a fresh descriptor.\n\nThe reverse direction is not the mirror image: when a plain number replaces\na running animation, the animation is cancelled and the number lands at\nonce it does not ease back, and no settle is reported. That matches\nupstream, which deletes the animation and pushes the plain value in the same\nmapper run rather than symmetrizing the two directions. On a driven property\nthe number reaches the widget on that frame; on a refused one, the snap is a\nReact render, produced for the caller automatically rather than waiting for\na cadence or a settle that will never come.\n\nA percentage or a color string in the previous result is not a starting\npoint a numeric driver can use, so those fall back to being seeded at the\ntarget.",
1172
1196
  },
1173
1197
  {
1174
- doc: "docs/research/navigation-extensibility.md",
1175
- heading: "4. Porting an existing react-navigation app",
1176
- text: "Compatible by construction: a real `@react-navigation/native` v8 peer, the\nofficial `useNavigationBuilder` and routers, a real `NavigationContainer`.\nEverything from react-navigation is imported from `@react-navigation/native`\ndirectly. We used to re-export a partial set from our navigation entry point\nand dropped it: the set was incomplete, so consumers ended up importing from\nboth places and could not tell which symbol came from where.\n\nMandatory changes: swap `createNativeStackNavigator` for our\n`createStackNavigator`; drop `react-native-screens`,\n`react-native-safe-area-context` and `react-native-gesture-handler` (all\nthree are mobile-native dependencies with nothing to bind to here).\n\nKeeping shared code portable: Linux-only options go behind a `.linux.tsx`\nplatform extension or `Platform.select({ linux: })`. Options a platform\ndoes not understand are ignored — and here, warned about in development.",
1198
+ doc: "docs/reference/reanimated-compat.md",
1199
+ heading: "Animating an SVG shape",
1200
+ text: '`useAnimatedProps` reads exactly as it does on mobile:\n\n```tsx\nimport { Circle, Svg } from "react-native-gtkx/svg"\nimport Animated, {\n useAnimatedProps,\n useSharedValue,\n withTiming,\n} from "react-native-reanimated"\n\nconst AnimatedCircle = Animated.createAnimatedComponent(Circle)\n\nconst Pulse = () => {\n const r = useSharedValue(10)\n const animatedProps = useAnimatedProps(() => ({ r: r.value }))\n return (\n <Svg\n width={100}\n height={100}\n onLayout={() => (r.value = withTiming(40))}\n >\n <AnimatedCircle\n cx={50}\n cy={50}\n fill="green"\n animatedProps={animatedProps}\n />\n </Svg>\n )\n}\n```',
1177
1201
  },
1178
1202
  {
1179
- doc: "docs/research/navigation-extensibility.md",
1180
- heading: "5. The desktop-RN landscape",
1181
- text: "No other desktop React Native platform has native navigation integration:\n\n- **react-native-screens** lists Windows support, but it is a thin\n old-architecture module; native-stack on modern react-native-windows\n fails, because screens has no new-architecture Windows implementation and\n RNW 0.82 removed the old one. Microsoft's own react-native-gallery falls\n back to the JS drawer.\n- **react-native-macos** is not supported by react-native-screens at all —\n AppKit has no navigation-stack primitive to bind to.\n\nOur path a real `Adw.NavigationView` driven from a custom navigator, with\nreact-navigation state as the source of truth and native pops reported back\ninto it is structurally the iOS native-stack / `UINavigationController`\nmodel, which neither desktop platform reached. GTK's advantage is that the\nprimitive exists at all: back button, Escape, back gesture, history menu and\ntransitions ship with the widget.\n\nThe trade: a JS stack can render anything into its fake header, while our\nchrome is real and had to be opened up deliberately which is what the\nintrinsic-size root does.\n\nSources: react-native-screens README and discussions #1575 / #2541, RNW\ndiscussions #14273 / issue #4152 / new-architecture docs / 0.82 release\npost, microsoft/react-native-gallery, reactnavigation.org native-stack docs.",
1203
+ doc: "docs/reference/reanimated-compat.md",
1204
+ heading: "Interpolation and color",
1205
+ text: "`interpolate`, `clamp` and `Extrapolation` (with its deprecated alias\n`Extrapolate`) are fully implemented, including per-edge extrapolation\nmodes. `Easing` is fully implemented, including `Easing.bezier`'s factory\nshape.\n\n`interpolateColor`, `convertToRGBA`, `isColor` and `rgbaArrayToRGBAColor` are\nfully implemented for the `'RGB'` color space (upstream's 2.2-gamma\ninterpolation) and `'HSV'` (upstream's hue-wrap correction), including\n`'transparent'` handling.\n\nDiffers from react-native-reanimated: the `'LAB'` color space throws by\nname. Upstream's `'LAB'` support is a vendored slice of the `culori` library\nfed 0-255 channels, where `culori` itself documents a 0-1 range — matching\nupstream here would mean matching that scaling bug rather than the color\nspace itself.\n\nDiffers from react-native-reanimated: `interpolateColor` only accepts color\nstrings as input, never `PlatformColor`. A theme color has no numeric value\nuntil GTK resolves it against the live Adwaita theme, so there is nothing to\nblend between keyframes; passing one throws, naming the case.\n\n`PlatformColor` is the platform's own: a theme color addressed by name,\nresolved by GTK against the live Adwaita palette (`var(--accent-bg-color)`\nand the rest). It can be animated _between_ on a shared value — assign one\n`PlatformColor` and then another, and a shared value transitions cleanly —\nbut it cannot be interpolated _through_, for the reason above.\n\nDiffers from react-native-reanimated: `processColor` and `DynamicColorIOS`\nthrow by name. `processColor` returns RN's packed AARRGGBB integer, whose\nonly real consumer is a native module that unpacks it; there is no native\nmodule here, a color's destination is a GTK stylesheet, and a stylesheet\ntakes strings. Refusing beats handing back a number nothing downstream would\naccept.",
1206
+ },
1207
+ {
1208
+ doc: "docs/reference/reanimated-compat.md",
1209
+ heading: "Gesture and scroll integration",
1210
+ text: "`useAnimatedRef` and `measure` are fully implemented, and callable from\nanywhere — there is no worklet boundary to be inside of. `measure` returns\n`null` before the first committed layout, matching RN's own contract. See\n[above](#measure-on-a-node-moved-this-way) for what it reports on a node\nwhose position or size is being driven.\n\n`useAnimatedScrollHandler` is fully implemented for `onScroll`, riding a\npath that already existed: `ScrollView`'s `emitScroll` runs from a\n`GtkAdjustment::value-changed` handler, a C callback on the same loop this\nJS runs on — so a handler that writes a shared value gets Reanimated's\npromise (no React render per scroll event) with no extra event machinery.\nHand the result to a scrollable's `onScroll` prop; the handler receives\nReanimated's flattened event shape (`event.contentOffset.y`, not\n`event.nativeEvent`) carrying the three measurements a `GtkScrolledWindow`\ncan report, plus one context object shared across every call.\n\n`onBeginDrag`, `onEndDrag`, `onMomentumBegin` and `onMomentumEnd` are all\ncalled: a mouse-wheel burst produces one synthetic begin/end pair with no\nmomentum phase, while a touchpad glide produces all four phases from its\nnative gesture sequence. The wheel pair is a documented desktop extension —\nRN has no wheel input to model. `contentInset`, `velocity` and `zoomScale`\nare absent from the event rather than invented as zero.\n\n`scrollTo(ref, x, y, animated)` is fully implemented against a\n`useAnimatedRef`-pointed scrollable: because this is the same thread that\nowns the widget, it calls the scrollable's own imperative `scrollTo`\nsynchronously.\n\nDiffers from react-native-reanimated: the argument order is upstream's\npositional form rather than RN's options object, so library call sites are\nunaffected. `animated` is accepted and ignored, matching `ScrollView`'s own\nbehavior. A ref pointing at nothing, or at a component with no scroll API,\nis silently ignored rather than throwing, matching upstream.\n\n`useScrollOffset` and `useScrollViewOffset` are fully implemented: a shared\nvalue that tracks a scrollable's current offset, updated directly from the\nadjustment's own `value-changed` signal — no `onScroll` prop required and no\nReact render per event. They take upstream's argument for writing into a\nshared value the caller already owns, and upstream's own axis rule (`x` when\na horizontal offset exists, `y` otherwise). Point one at a `ScrollView`, a\n`FlatList` (which resolves through to the `ScrollView` it renders\ninternally), or an `Animated.ScrollView`; pointing one at anything else\nwarns once and the value stays `0`. Cost is about 5 µs per scroll event\nwhile tracking, and nothing while not — the hook connects on mount and\ndisconnects on unmount.\n\n`useHandler` is fully implemented. Its `doDependenciesDiffer` is always\n`false` — not a stub, a statement: upstream needs that check because a\nworklet is a by-value snapshot that can go stale, and here a handler is an\nordinary closure read out of a ref at call time, so it never goes stale.\n`useWeb` reports `true`, for the same reason the whole surface sits on\nupstream's own web implementation.\n\n`useEvent` is implemented for scroll event names only — `onScroll`,\n`onScrollBeginDrag`, `onScrollEndDrag`, `onMomentumScrollBegin`,\n`onMomentumScrollEnd`. The value it returns goes straight on a scrollable's\n`onScroll` prop, which is the actual subscription mechanism here — the same\nobject `useAnimatedScrollHandler` returns, so a hand-built handler and the\nstock one behave identically.\n\nDiffers from react-native-reanimated: any other event name throws where it\nis requested, naming itself — there is no native event registry to\nsubscribe an arbitrary event name against, and a subscription that could\nnever fire is exactly the failure mode this package refuses everywhere else.\n`rebuild` is accepted and ignored, for the same reason `doDependenciesDiffer`\nis always false. `.workletEventHandler` throws — it exists upstream to\nregister a native view tag, and there is neither a native view nor a tag\nhere.",
1211
+ },
1212
+ {
1213
+ doc: "docs/reference/reanimated-compat.md",
1214
+ heading: "Threads: `runOnUI` / `runOnJS`",
1215
+ text: "There is one thread, so `runOnUI` and `runOnJS` have nothing to cross — but\nthey are not inlined. Both schedule rather than run immediately, and both\nreturn `void`, matching upstream. A \"UI\" hop is queued as a task and an \"RN\"\nhop as a microtask, so a UI hop still resolves later than an RN hop queued\nat the same instant — the same relative order upstream produces — without\nwaiting for an animation frame the way upstream's own web build does\n(`requestAnimationFrame` stands in there for a UI runtime the web doesn't\nhave; React Native's real UI thread does not wait for one either, and\nneither does this platform).\n\nWaiting for a frame that never needed to be waited for is not cosmetic: a\n`scheduleOnUI(measure)` / `scheduleOnRN(use the result)` round trip that\nwaits a full frame is longer than the gap between two GTK pointer events,\nand produces an observable hover-flicker in drag interactions if it\nregresses.\n\n`scheduleOnUI` and `scheduleOnRN` are `react-native-worklets`' own names for\nthe same mechanism, and are the very same functions re-exported — see\n[Worklets](#worklets-react-native-gtkxworklets) below.",
1216
+ },
1217
+ {
1218
+ doc: "docs/reference/reanimated-compat.md",
1219
+ heading: "`Animated.View`, `Animated.Text`, `Animated.Image`, `Animated.ScrollView`",
1220
+ text: "`Animated.View` is the platform's own `View`, unchanged, taking a `ref` that\ngives `measure`/`measureInWindow`/`measureLayout`. `Animated.Text`,\n`Animated.Image` and `Animated.ScrollView` are `createAnimatedComponent`\nover the platform's own components — no subclass, no special case — and all\nthree forward `ref` through, so `useAnimatedRef` + `measure()` works on them\nexactly as on `Animated.View`.\n\n`createAnimatedComponent` adds no widget to the tree. It renders the\ncomponent it wraps and reaches that component's widget through the `ref` it\nalready exposes, so the GTK output is exactly what the unwrapped component\nproduces — wrapping a component in an extra layer would change flex layout\nfor its children and change what `measureLayout` is relative to, which is a\ndifferent tree, not a shim. Wrap anything that takes a `ref` exposing the\ngeometry methods; anything else gets a named warning rather than a silent\nno-op.\n\nDiffers from react-native-reanimated: `Animated.FlatList` throws by name\nrather than working. Unlike `View`/`Text`/`Image`/`ScrollView`, `FlatList`\nis a composite over a windowed core over a `ScrollView` — the `ScrollView`\nis the only thing in that chain that owns a widget, and `FlatListHandle` is\na scroll API by contract, so there is no widget to read back out of its\nref. Upstream's `Animated.FlatList` mostly exists so `onScroll` can be an\n`Animated.event`/`useAnimatedScrollHandler`, and that hook is implemented\nhere directly — a plain `FlatList` already takes it on its own `onScroll`\nprop and needs no animated wrapper for it. Put an animated style on an\n`Animated.View` around the list, or use `Animated.ScrollView` when\nvirtualization isn't needed.\n\n`addWhitelistedNativeProps` and `addWhitelistedUIProps`, both reachable off\nthe default export, are accepted and do nothing — documented no-ops\nupstream too, since the allow-lists they used to write to no longer exist in\nReanimated itself. They are kept callable so startup code that calls them\ndoes not fail on a line that already did nothing upstream.",
1221
+ },
1222
+ {
1223
+ doc: "docs/reference/reanimated-compat.md",
1224
+ heading: "Layout animations",
1225
+ text: "![The gallery's Layout animations section: FadeIn/FadeOut/LinearTransition reordering rows, and the layout-animation preset catalog.](../shots/gallery/reanimated-layout.png)\n\n`entering`, `exiting` and `layout` props work on every animated component —\n`Animated.View`, `Animated.Text`, `Animated.Image`, `Animated.ScrollView`,\nand anything wrapped with `createAnimatedComponent` — because they are added\nby wrapping a component rather than by subclassing it, and the wrapper adds\nno widget to the tree, exactly like `createAnimatedComponent` itself.\n\n```tsx\n<Animated.View\n entering={FadeIn.duration(300)}\n exiting={FadeOut}\n layout={LinearTransition.springify()}\n/>\n```\n\n`entering` writes the builder's initial values in the same commit that\nmounts the widget, so it is never drawn un-faded, not even for one frame,\nand animates from there. `layout` watches for the layout engine committing a\ndifferent rect for that child, and walks it from where it was to where the\nengine put it.\n\nDiffers from react-native-reanimated: `layout` animates the position as a\ntranslation and applies a size change immediately, rather than animating\nboth. Upstream's `LinearTransition` animates `originX`/`originY`/`width`/\n`height` together; here, the origins are still honored as a translation —\ncomposed with whatever transform the style already has, so a row that\nscales while a list reorders does both — but a size change lands on the\nnext commit instead of animating, for the same reason `useAnimatedStyle`\nrefuses to drive most sizes: animating a size means a Yoga pass whose cost\nis the tree's, not the animated value's. `CurvedTransition`'s\n`.easingWidth()`/`.easingHeight()` are accepted and ignored for the same\nreason; its two position easings are honored.",
1226
+ },
1227
+ {
1228
+ doc: "docs/reference/reanimated-compat.md",
1229
+ heading: "`exiting` and widget retention",
1230
+ text: "`exiting` needed a primitive nothing else in this surface required. An exit\nanimation has to keep drawing a widget that React has already reconciled\naway, and React's deletion is neither asynchronous nor negotiable — in one\nsynchronous commit it runs the unmounting subtree's cleanup and unparents\nits topmost widget. The platform holds the widget through a\n**widget-retention** mechanism, the same one `react-native-gtkx/adw`'s\n`NavigationStack` uses to keep a page on screen while it slides out:\n\n- The widget is put back into the same container, at the end of the child\n list, so it draws over the siblings closing the gap rather than under\n them.\n- Its Yoga node leaves the layout tree immediately, so an exiting view does\n not hold its space open — the row below it moves up at once, and the exit\n animation plays over the top.\n- Every container inside the retained subtree keeps its layout manager\n until the animation ends, so the exiting view's own children stay exactly\n where they were.\n- A fallback timer always runs, armed from the animation's declared length.\n Whichever arrives first — the animation's real end or the timer — drops\n the widget, so a spring that never settles, a dead frame source, or an\n animation that never started cannot leave a widget parented, drawn and\n hit-testable forever.\n\n`exiting` is skipped when the component's own container is unmounting in\nthe same commit — there is no container left to hold the widget in, and an\nexit animation inside a disappearing parent has no one left to be seen by.",
1231
+ },
1232
+ {
1233
+ doc: "docs/reference/reanimated-compat.md",
1234
+ heading: "`Keyframe` and the `*Transition` builders",
1235
+ text: "`FadeIn`, `FadeOut`, `LinearTransition` (and its deprecated alias `Layout`)\nand `Keyframe` are fully implemented, exposing upstream's fluent surface —\n`.duration()`, `.delay()`, `.easing()`, `.springify()` and the spring\nparameters, `.rotate()`, `.withInitialValues()`, `.withCallback()` — usable\nas the class itself or as a built instance.\n\n`BaseAnimationBuilder` and `ComplexAnimationBuilder` both resolve to one\nclass. Upstream splits the plain chain from the spring-parameter chain into\ntwo classes; this platform does not, and a library subclassing either name\nkeeps working.\n\nFour more `layout` transitions beside `LinearTransition` are fully\nimplemented: `CurvedTransition`, `FadingTransition`, `JumpingTransition`,\n`SequencedTransition`, plus `EntryExitTransition`, which composes an\nentering builder and an exiting builder into one layout animation. Each\nfollows the same paint-only position rule as `LinearTransition` above.\n\n`LayoutAnimationConfig` is fully implemented: `<LayoutAnimationConfig\nskipEntering skipExiting>` suppresses the animations of the subtree below\nit and adds no widget. `enableLayoutAnimations` warns and does nothing,\nmatching upstream exactly, where it is deprecated and its allow-list is\ngone.",
1236
+ },
1237
+ {
1238
+ doc: "docs/reference/reanimated-compat.md",
1239
+ heading: "The preset catalog",
1240
+ text: "60 of upstream's 76 layout-animation presets are implemented, on upstream's\nown parameters, sharing one builder class over a parameter table. (`FadeIn`\nand `FadeOut` themselves ship as the hand-written base builders described\nabove, alongside `Keyframe` and `LinearTransition`, rather than as table\nentries — the family below covers the rest of upstream's `Fade*` set.)\n\n| Family | Presets |\n| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Bounce (10) | `BounceIn`, `BounceInDown`, `BounceInLeft`, `BounceInRight`, `BounceInUp`, `BounceOut`, `BounceOutDown`, `BounceOutLeft`, `BounceOutRight`, `BounceOutUp` |\n| Fade (8) | `FadeInDown`, `FadeInLeft`, `FadeInRight`, `FadeInUp`, `FadeOutDown`, `FadeOutLeft`, `FadeOutRight`, `FadeOutUp` |\n| Pinwheel (2) | `PinwheelIn`, `PinwheelOut` |\n| Roll (4) | `RollInLeft`, `RollInRight`, `RollOutLeft`, `RollOutRight` |\n| Rotate (8) | `RotateInDownLeft`, `RotateInDownRight`, `RotateInUpLeft`, `RotateInUpRight`, `RotateOutDownLeft`, `RotateOutDownRight`, `RotateOutUpLeft`, `RotateOutUpRight` |\n| Slide (8) | `SlideInDown`, `SlideInLeft`, `SlideInRight`, `SlideInUp`, `SlideOutDown`, `SlideOutLeft`, `SlideOutRight`, `SlideOutUp` |\n| Stretch (4) | `StretchInX`, `StretchInY`, `StretchOutX`, `StretchOutY` |\n| Zoom (16) | `ZoomIn`, `ZoomInDown`, `ZoomInEasyDown`, `ZoomInEasyUp`, `ZoomInLeft`, `ZoomInRight`, `ZoomInRotate`, `ZoomInUp`, `ZoomOut`, `ZoomOutDown`, `ZoomOutEasyDown`, `ZoomOutEasyUp`, `ZoomOutLeft`, `ZoomOutRight`, `ZoomOutRotate`, `ZoomOutUp` |\n\nThe 16 presets not implemented — the twelve `Flip*` and four `LightSpeed*`\n— are covered in [What is not implemented](#what-is-not-implemented) below.\n`rotate` on any preset or builder is carried as degrees rather than\nupstream's `'90deg'`/`'5rad'` strings — a numeric animation cannot carry a\nunit, and the matrix that reaches GTK is identical either way. A builder's\nown `.rotate()` and a `.withInitialValues()` angle still accept either\nspelling.",
1241
+ },
1242
+ {
1243
+ doc: "docs/reference/reanimated-compat.md",
1244
+ heading: "`useAnimatedKeyboard`",
1245
+ text: "`useAnimatedKeyboard` returns real shared values — `height` and `state` —\nthat are honored and never updated, the same shape and the same reason as\nthe portable `Keyboard` API's own desktop semantics: every number this hook\nreports describes a software panel sliding over the app and taking screen\nspace from it, and a desktop has no such panel. `height` reads `0` because\nthe keyboard occupies nothing, and `state` reads `KeyboardState.CLOSED`\nbecause it is — deliberately not `UNKNOWN`, which upstream seeds only until\nthe native side reports and which would be false here permanently.\n\nBoth are real shared values, not frozen constants: a `useAnimatedStyle`\nreading them subscribes, computes and settles exactly once, so a layout\nthat offsets itself by `keyboard.height.value` lands where it should rather\nthan throwing. An app written for three platforms keeps one source and gets\nthe right answer on this one too. `options` (upstream's Android\ntranslucency configuration) is accepted and ignored — it describes how the\nkeyboard's rectangle relates to a system bar, and there is neither.",
1246
+ },
1247
+ {
1248
+ doc: "docs/reference/reanimated-compat.md",
1249
+ heading: "Version reporting, logging and reduced motion",
1250
+ text: "`reanimatedVersion` reports `\"4.5.3\"` — the upstream version this surface's\nAPI mirrors, not a claim to literally be that package. Libraries that gate\nbehavior on a version number read this and take the right branch.\n`isConfigured` and `isReanimated3` both return `true` — upstream's own\ndeprecated presence checks, and the honest answer here is yes.\n\n`configureReanimatedLogger` is accepted and does nothing: there is no second\nReanimated logger to configure here, and refusing the call would break\nstartup code that calls it for a setting that changes nothing.\n`ReanimatedLogLevel` is mirrored as plain data (`warn = 1`, `error = 2`).\n\n`ReduceMotion` is mirrored as an enum (`System`, `Always`, `Never`), and\nevery value behaves as `Never`; `useReducedMotion()` always returns `false`.\nNo reduce-motion source is wired up on this platform yet — GNOME's\n`gtk-enable-animations` setting is the signal to read once it is.",
1251
+ },
1252
+ {
1253
+ doc: "docs/reference/reanimated-compat.md",
1254
+ heading: "Test helpers",
1255
+ text: "`withReanimatedTimer`, `advanceAnimationByTime` and `advanceAnimationByFrame`\nare real, not an emulation: the frame driver every animation on this\nplatform runs on is the platform's own, so a test takes that same driver and\nsteps it directly, rather than upstream's approach of faking Jest's timers\nand synthesizing frames on top of them. `withReanimatedTimer` also accepts\nan async body.\n\nAssert against the widget once the clock has been stepped —\n`widget.getOpacity()`, `widget.computeBounds(stage)`, `widget.measure()` —\nrather than reading a style object back. Driven by `withReanimatedTimer` +\n`advanceAnimationByTime`, those reads are deterministic. See\n[What is not implemented](#what-is-not-implemented) for `getAnimatedStyle`\nand `setUpTests`, which read a style back and are refused.",
1256
+ },
1257
+ {
1258
+ doc: "docs/reference/reanimated-compat.md",
1259
+ heading: "What is not implemented",
1260
+ text: "Each of these throws when called, rendered, or — for the handful that are\nplain values upstream — merely accessed, naming itself in the message. A\nsymbol not listed at all in the module fails earlier still, at bundle time,\nwith the bundler's own \"no export named X\".\n\n- **Color packing for a native module** — `processColor`, `DynamicColorIOS`.\n Both exist to hand a value to a native module; there is no native module\n here, and a GTK stylesheet takes strings, not packed integers.\n- **3D and skewed layout-animation presets** — the twelve `Flip*`\n (`FlipInEasyX`/`Y`, `FlipInXDown`/`Up`, `FlipInYLeft`/`Right`,\n `FlipOutEasyX`/`Y`, `FlipOutXDown`/`Up`, `FlipOutYLeft`/`Right`) and four\n `LightSpeed*` (`LightSpeedInLeft`/`Right`, `LightSpeedOutLeft`/`Right`).\n `Flip*` needs a real 3D rotation (`perspective` plus `rotateX`/\n `rotateY`); this platform folds every transform into one 2D affine\n matrix, which has no third axis. `LightSpeed*` needs `skewX`, which is\n left out of the platform's whole transform surface on purpose, not only\n from this catalog.\n- **Shared element transitions** — `SharedTransition`,\n `SharedTransitionBoundary`. Needs a `sharedTransitionTag` prop, an\n overlay layer above the navigation stack, and a retention primitive\n that reparents the leaving widget — none of which exist. The platform's\n own retention primitive (used by `exiting`, above) deliberately holds a\n widget in its own parent instead. Upstream's own web build does not\n implement this either.\n- **Reanimated 4's CSS animations** — `css`, `createCSSAnimatedComponent`,\n `cubicBezier`, `linear`, `steps`. Not reached by this surface.\n- **Sensor, composed-event and frame-callback hooks** —\n `useAnimatedSensor`, `useComposedEventHandler`, `useFrameCallback`,\n `useTimestamp`. No sensor source and no per-frame callback registry on\n this platform.\n- **Worklet-runtime primitives** — `createWorkletRuntime`, `runOnRuntime`,\n `executeOnUIRuntimeSync`. A second runtime is structural — there is one\n thread here, and upstream's own non-native `runtimes.ts` throws for\n these too on a single-runtime build.\n- **Native-module-only functions** — `dispatchCommand`,\n `getRelativeCoords`, `setGestureState`, `setNativeProps`, `getViewProp`,\n `createAnimatedPropAdapter`, `NativeEventsManager`,\n `getUseOfValueInStyleWarning`. Each crosses to a native view manager\n that does not exist here.\n- **Orientation and sensor enums** — `InterfaceOrientation`,\n `IOSReferenceFrame`, `SensorType`. No source of truth for any of them on\n a desktop.\n- **Screen transitions** — `ScreenTransition`, `startScreenTransition`,\n `finishScreenTransition`. Not reached by this surface.\n- **Dev tooling** — `PerformanceMonitor`, `ReducedMotionConfig`,\n `getDynamicFeatureFlag`, `getStaticFeatureFlag`, `setDynamicFeatureFlag`.\n Not reached by this surface.\n- **Style read-back** — `getAnimatedStyle`, `setUpTests`. Upstream's\n `getAnimatedStyle` returns the style object its updater produced, which\n exists on mobile only because its Jest path mirrors it onto the\n component. Here a style is taken apart at bind time — opacity to the\n widget, colors to a private CSS provider, the whole `transform` array\n folded into one matrix in the rect store — so there is no such object\n left to return, at any point after bind time. `setUpTests` exists only\n to install `toHaveAnimatedStyle`/`toHaveAnimatedProps`, both\n `getAnimatedStyle` under a matcher.\n- **Definition helper** — `defineAnimation`. Not reached by this surface.\n- **Animated component** — `Animated.FlatList`. A composite with no widget\n of its own to expose through a ref — see\n [Animated.View, Animated.Text, Animated.Image, Animated.ScrollView](#animatedview-animatedtext-animatedimage-animatedscrollview)\n above.",
1261
+ },
1262
+ {
1263
+ doc: "docs/reference/reanimated-compat.md",
1264
+ heading: "Worklets (`react-native-gtkx/worklets`)",
1265
+ text: "Reanimated 4 moved its worklet surface out of Reanimated and into its own\npackage, `react-native-worklets`, and libraries increasingly import it under\nthat name directly rather than through Reanimated. Aliasing\n`react-native-reanimated` alone leaves that import wall standing one\npackage over — and it is an import-time wall, not a runtime one:\n`react-native-reanimated-dnd` 2.0.0 pulls `scheduleOnRN` and `scheduleOnUI`\nout of `react-native-worklets` at module scope, in five of its hooks\n(`useDraggable`, `useDroppable`, `useSortable`, `useHorizontalSortable`,\n`useGridSortable`), with no `try { require } catch` guarding any of them —\nso an unaliased package name fails the whole module at import time rather\nthan at the point a function is called. Both the vite and Metro presets\nalias `react-native-worklets` onto `react-native-gtkx/worklets`, so an app\nkeeps its source unchanged.\n\nThe surface itself already exists inside the Reanimated subpath; this\npackage adds the _name_. `runOnUI`, `scheduleOnUI`, `runOnJS` and\n`scheduleOnRN` reached through either package name are the same instance,\nnot two copies — a job queued through one lands in the same batch, in the\nsame order, as a job queued through the other. Upstream has this same\nproperty for the same reason: Reanimated re-exports these functions from\n`react-native-worklets` rather than keeping a second copy of them.\n\nWhat this package implements and what it refuses is decided by upstream's\nown non-native build — the `.ts` files `react-native-worklets` ships\nalongside its `.native.ts` ones, which is what react-native-windows and the\nweb run on. Where that build computes something, so does this subpath;\nwhere it throws, this subpath refuses by the same name. A worklet runtime is\na second JS runtime, and this platform has one thread, which is where the\nboundary actually is. Measured against `react-native-worklets` 0.11.3.\n\nOne thing upstream's non-native build does that is deliberately not copied:\nits UI hop waits for a `requestAnimationFrame`, standing in for a UI runtime\nthe web hasn't got. React Native's real UI thread does not wait for a\nframe, and neither does this platform.\n\n| Export | Behavior |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `runOnUI`, `scheduleOnUI`, `runOnJS`, `scheduleOnRN` | Deferred, not inlined, returning `void` — the same functions `react-native-gtkx/reanimated` exports (see [Threads](#threads-runonui--runonjs) above). |\n| `runOnUIAsync` | Resolves with the worklet's return value once the UI hop runs it — the one thread-crossing function that hands anything back, because a promise can cross the deferral the others impose. |\n| `isWorkletFunction` | Upstream's `__workletHash` check. This platform never runs the Babel plugin, so nothing is a worklet by that test and nothing needs to be — `'worklet'` is an inert string. |\n| `makeShareableCloneRecursive`, `createSerializable`, `makeShareable`, `makeShareableCloneOnUIRecursive`, `isSerializableRef`, `isShareableRef` | Identity, matching upstream's own non-native serializer: a value never leaves the runtime it was made in, so there is nothing to clone. |\n| `serializableMappingCache`, `shareableMappingCache`, `registerCustomSerializable`, `callMicrotasks` | No-ops, matching upstream. |\n| `isShareable`, `isSynchronizable` | Upstream's structural checks, ported unchanged. |\n| `RuntimeKind`, `getRuntimeKind`, `isRNRuntime`, `isUIRuntime`, `isWorkerRuntime`, `isWorkletRuntime`, `UIRuntimeId` | Answer for the one runtime there is: `ReactNative`. Matches upstream's own non-native path, whose initializer sets that kind once and nothing ever changes it. |\n| `getStaticFeatureFlag`, `getDynamicFeatureFlag`, `setDynamicFeatureFlag`, `isBundleModeEnabled`, `toggleSlowAnimationsOnUIRuntime` | `false` and no-ops — these gate upstream's native experiments and its Babel bundle mode, neither of which exists here. |\n| `createWorkletRuntime`, `runOnRuntime`, `runOnRuntimeSync`, `runOnRuntimeAsync` (and the `WithId` variants), `scheduleOnRuntime` (and its `WithId` variant), `getUIRuntimeHolder`, `getUISchedulerHolder` | Throw, naming themselves. A second runtime is structural; upstream's own `runtimes.ts` throws for every one of these on a single-runtime build too. |\n| `runOnUISync`, `executeOnUIRuntimeSync` | Throw. Both mean \"run this over there and give me the answer synchronously\"; deferring instead would be worse than refusing, since the caller wants a return value and a deferred call has none. |\n| `createShareable`, `createSynchronizable` | Throw — both are memory shared between runtimes, and there is one runtime. |\n| `WorkletsModule` | Throws, naming itself — the one deliberate deviation from mirroring upstream exactly: upstream's non-native build exports this as `null`, which fails by naming nothing at the call site. |\n\nTwo measurements, taken against the published packages rather than their\ndocumentation:\n\n- `react-native-reanimated-dnd` 2.0.0 imports exactly two symbols from\n `react-native-worklets` — `scheduleOnRN` and `scheduleOnUI` — both\n implemented here.\n- `@gorhom/bottom-sheet` 5.2.14 imports nothing from\n `react-native-worklets`; it reaches `runOnJS`/`runOnUI` through\n `react-native-reanimated` and does not depend on the worklets package at\n all. `react-native-gesture-handler` 3.1.0 does use `scheduleOnUI` from\n this package, but behind a `try { require } catch`, so it was never\n exposed to the unaliased-import failure mode described above.\n\nA symbol not listed anywhere in this section fails at bundle time, with the\nbundler's own \"no export named X\" — the same behavior as the Reanimated\nsubpath.",
1266
+ },
1267
+ {
1268
+ doc: "docs/reference/styling.md",
1269
+ heading: "Layout (routed to Yoga)",
1270
+ text: "![The gallery's Layout section: flexDirection, justifyContent and alignItems arranging boxes under Yoga.](../shots/gallery/layout.png)\n\n`alignContent`, `alignItems`, `alignSelf`, `aspectRatio`, `bottom`,\n`columnGap`, `direction`, `display`, `flex`, `flexBasis`, `flexDirection`,\n`flexGrow`, `flexShrink`, `flexWrap`, `gap`, `height`, `justifyContent`,\n`left`, `margin`, `marginBottom`, `marginHorizontal`, `marginLeft`,\n`marginRight`, `marginTop`, `marginVertical`, `maxHeight`, `maxWidth`,\n`minHeight`, `minWidth`, `overflow`, `padding`, `paddingBottom`,\n`paddingHorizontal`, `paddingLeft`, `paddingRight`, `paddingTop`,\n`paddingVertical`, `position`, `right`, `rowGap`, `top`, `width` — all\n**supported**, with behavior defined by the layout engine itself.\n\n![The gallery's Clipping section: overflow: \"hidden\" cutting a View to its rounded shape, with hit-testing stopping at the same clip.](../shots/gallery/clipping.png)\n\n`overflow` is the one key that does not stop at Yoga: Yoga needs it while\nmeasuring (a scroll node's main axis is unconstrained), and the widget needs\nit to clip. GTK4 CSS has no `overflow` property, so this is a direct widget\ncall rather than a CSS declaration — the only style outside `transform` whose\nGTK half is not CSS. `hidden` clips paint and hit-testing to the widget's CSS\npadding box, rounded by `borderRadius`; `scroll` clips the same way and adds\nno scrolling of its own — matching RN, where only a `ScrollView` scrolls;\n`visible` (the default) lets children paint past the container, as in RN.",
1271
+ },
1272
+ {
1273
+ doc: "docs/reference/styling.md",
1274
+ heading: "Visual (routed to GTK CSS)",
1275
+ text: "- **`backgroundColor`** — supported: `background-color`.\n- **`opacity`** — supported: `opacity`, clamped to `[0, 1]`.\n- **`boxShadow`** — supported: `box-shadow`, from either RN form — a CSS\n string or a `BoxShadowValue[]` array. A string is parsed rather than\n forwarded, so colors go through the same color parser (a `PlatformColor`\n works inside a shadow) and a malformed shadow is dropped with one\n development warning instead of corrupting the declaration block. Lengths\n follow RN's own grammar — a bare number or `px`, nothing else; blur may\n not be negative. Differs from react-native: an omitted `color` renders as\n black, not `currentColor` — RN's own documented deviation from CSS, kept\n here on purpose. Shadow order is CSS's: the first one paints on top.\n- **`outlineWidth`** — supported: `outline-width`; an unset `outlineStyle`\n becomes `solid` automatically once the width is greater than 0 (GTK, like\n the web, otherwise defaults to `none`). Takes no layout space, so unlike\n `borderWidth` it never reaches Yoga.\n- **`outlineColor` / `outlineOffset` / `outlineStyle`** — supported:\n `outline-color` / `outline-offset` (px, negative allowed) /\n `outline-style` (`solid`/`dotted`/`dashed`); an explicit style always wins\n over the automatic solid.\n- **`borderWidth`** — supported: `border-width`; an unset `borderStyle`\n becomes `solid` automatically once any border width is greater than 0\n (GTK otherwise defaults to `none`).\n- **`borderTopWidth` / `borderRightWidth` / `borderBottomWidth` /\n `borderLeftWidth`** — supported: per-side `border-*-width`, emitted after\n the shorthand and overriding it; each also triggers the automatic solid\n style.\n- **`borderColor`** — supported: `border-color`; with no width the border\n stays invisible, since the default width is 0, as in RN.\n- **`borderTopColor` / `borderRightColor` / `borderBottomColor` /\n `borderLeftColor`** — supported: per-side `border-*-color`, emitted after\n the shorthand and overriding it.\n- **`borderStyle`** — supported: `border-style`\n (`solid`/`dotted`/`dashed`); an explicit value wins over the automatic\n solid.\n- **`borderRadius`** — supported: `border-radius`.\n- **`borderTopLeftRadius` / `borderTopRightRadius` /\n `borderBottomRightRadius` / `borderBottomLeftRadius`** — supported:\n per-corner `border-*-radius`, emitted after the shorthand and overriding\n it.\n- **`color`** — supported: `color`.\n- **`fontFamily`** — supported: `font-family` (a name containing spaces is\n quoted).\n- **`fontSize`** — supported: `font-size`, in px.\n- **`fontStyle`** — supported: `font-style`.\n- **`fontWeight`** — supported: `font-weight` (both keyword and\n numeric-string forms, `\"100\"`–`\"900\"`).\n- **`letterSpacing`** — supported: `letter-spacing`, in px.\n- **`lineHeight`** — partial: `line-height`, in px (GTK ≥ 4.6). Matches\n RN's \"line height in points\" semantics; RN's multiplier form is not\n supported.\n- **`textAlign`** — partial: applied by `Text` directly rather than\n through CSS, since GTK4 CSS has no `text-align` — resolved to a label's\n own `xalign`/`justification` properties.\n- **`textDecorationLine`** — partial: applied by `Text` directly rather\n than through CSS, since GTK4 has no widget-level `text-decoration` —\n resolved to Pango's own `underline`/`strikethrough` attributes, which\n also reserve room below the baseline for an underline where one is\n measured.\n- **`transform`** — partial: never reaches CSS (GTK4 has no widget\n `transform` property) — the array is folded into one matrix and handed\n to the container's own allocation. `translateX`/`translateY`, `scale`,\n `scaleX`, `scaleY`, `rotate`/`rotateZ` only; no 3D, no skew, no `matrix`,\n no `transformOrigin` — see\n [Components](components/index.md#layout-paint-and-hit-testing).",
1276
+ },
1277
+ {
1278
+ doc: "docs/reference/styling.md",
1279
+ heading: "Colors",
1280
+ text: '| Format | Status | Note |\n| ------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |\n| named colors (CSS Color 4), `transparent` | supported | normalized to `rgb()`/`rgba()` |\n| `#rgb` / `#rgba` / `#rrggbb` / `#rrggbbaa` | supported | normalized to `rgb()`/`rgba()` |\n| `rgb()` / `rgba()` | supported | both comma and space syntax (`rgb(255 0 0 / 0.5)`); channels as numbers or percentages |\n| `hsl()` / `hsla()` | supported | hue as a number or with `deg`; saturation/lightness strictly as percentages; converted to `rgb()`/`rgba()` |\n| `PlatformColor("accent-bg-color", ...)` | supported | resolves to `var(--accent-bg-color, ...)` — Adwaita theme variables (libadwaita ≥ 1.6); `@name` reaches a legacy GTK named color as the fallback |\n| `var(--...)` / `@name` as a plain string | supported | passed through without normalization |\n| an invalid string | ignored | the color parses to nothing; the declaration is dropped, with one development warning per distinct value |',
1281
+ },
1282
+ {
1283
+ doc: "docs/reference/styling.md",
1284
+ heading: "Not part of the style contract",
1285
+ text: "Any key outside `LayoutStyle`/`VisualStyle`/`BehavioralStyle` — `elevation`,\n`filter`, `mixBlendMode`, `textTransform`, `tintColor`, and the like — is\naccepted, warned about once per key, and dropped. `pointerEvents` and\n`zIndex` are `BehavioralStyle`: each is read by the component that implements\nthe behavior, not by either half of this pipeline (see\n[Components](components/index.md#layout-paint-and-hit-testing) for both).",
1286
+ },
1287
+ {
1288
+ doc: "docs/reference/svg.md",
1289
+ heading: "Import and aliasing",
1290
+ text: "`react-native-gtkx/svg` re-exports its component set in `react-native-svg`'s\nown shape: `Svg` as both the default and a named export, everything else\nnamed. The `react-native-gtkx/metro` and `react-native-gtkx/vite` presets\nalias the bare `react-native-svg` package name onto this subpath\nautomatically, the same way they alias `react-native` itself, so portable\ncode that imports from `react-native-svg` runs unmodified. Apps using\nneither preset can point their own bundler alias at `react-native-gtkx/svg`\nby hand. `react-native-svg` itself is never a dependency of this package and\ndoes not need to be installed — the alias works whether or not the real\npackage is present.\n\n`react-native-gtkx/dnd` follows the exact same aliasing pattern for\n`react-native-reanimated-dnd`; see [dnd.md](dnd.md) if drag-and-drop is also\npart of the app being ported.",
1291
+ },
1292
+ {
1293
+ doc: "docs/reference/svg.md",
1294
+ heading: "`Svg`",
1295
+ text: 'The root component. It is a Yoga leaf, sized entirely by style/flex — like\n`Image`, never by measuring the widget, so nothing here is intrinsic-sized.\n\n| Prop | Behavior |\n| --------------------- | --------------------------------------------------------------------------------------------------------------- |\n| `width` / `height` | Convenience props layered onto `style`, sizing the leaf. |\n| `style` | The general sizing/layout escape hatch, same as any other view. |\n| `viewBox` | `"minX minY width height"`. Reshapes the internal coordinate system exactly like real SVG — Yoga never sees it. |\n| `preserveAspectRatio` | `xMin`/`xMid`/`xMax` × `YMin`/`YMid`/`YMax`, `meet`/`slice`, or `none`; defaults to `xMidYMid meet`. |\n\nContent always clips to the allocated bounds. There is no `overflow: visible`\nopt-out.',
1296
+ },
1297
+ {
1298
+ doc: "docs/reference/svg.md",
1299
+ heading: "Shapes",
1300
+ text: '- **`Path`** — `d` is handed straight to `Gsk.Path.parse()`, which\n understands SVG path syntax natively. There is no path parser of this\n project\'s own.\n- **`Rect`** — `x`/`y`/`width`/`height`/`rx`/`ry`.\n- **`Circle`** — `cx`/`cy`/`r`.\n- **`Ellipse`** — `cx`/`cy`/`rx`/`ry`.\n- **`Line`** — `x1`/`y1`/`x2`/`y2`. Stroke-only: there is no `fill` prop at\n all on `Line`, not even one that is silently ignored.\n- **`Polygon`** / **`Polyline`** — `points`, either `"x,y x,y …"` or a space\n -separated equivalent; closed and open respectively.\n\nEvery shape other than `Path` is a small geometry helper away from the same\n`d` syntax, so all of them end up drawn through that one `Gsk.Path.parse()`\ncall.',
1301
+ },
1302
+ {
1303
+ doc: "docs/reference/svg.md",
1304
+ heading: "Paint props",
1305
+ text: 'Every shape accepts the same paint props:\n\n| Prop | Behavior |\n| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `fill` / `stroke` | A static CSS color — hex, `rgb()`, `hsl()`, a named color, `transparent`, `none`, or `"url(#id)"` referencing a gradient. Defaults match SVG: `fill="black"`, `stroke="none"`. |\n| `fillRule` | `nonzero` \\| `evenodd`. |\n| `fillOpacity` / `strokeOpacity` / `opacity` | Independent opacity channels. |\n| `strokeWidth` | Stroke thickness. |\n| `strokeLinecap` / `strokeLinejoin` | Line cap and join style. |\n| `strokeDasharray` / `strokeDashoffset` | Dash pattern and its offset. |\n\nAn unresolvable `url(#id)` reference paints nothing for that fill/stroke\nrather than throwing.',
1306
+ },
1307
+ {
1308
+ doc: "docs/reference/svg.md",
1309
+ heading: "Grouping and transforms (`G`)",
1310
+ text: "`G` groups children under an `opacity` and/or a `transform` string:\n`translate()`, `scale()`, `rotate()`, `rotate(a, cx, cy)`, and `matrix()` —\nthe plain SVG transform-list syntax. `matrix()` maps directly onto\n`Gsk.Transform.matrix2d()`.\n\nDiffers from react-native-svg: `skewX`/`skewY` and the structured\n`transform={[{ translateX: ... }]}` array form that `Animated.View` accepts\nelsewhere in this platform are not supported on `G` — only the string form.",
1311
+ },
1312
+ {
1313
+ doc: "docs/reference/svg.md",
1314
+ heading: "Gradients",
1315
+ text: '`<Defs>` holds gradient definitions and must be a direct child of `Svg`;\nnested `Defs` are not scanned.\n\n- **`<LinearGradient id x1 y1 x2 y2>`** and **`<RadialGradient id cx cy r>`**\n take fractions from 0 to 1 by default (`gradientUnits="objectBoundingBox"`,\n mapped against the shape\'s own `Gsk.Path.getBounds()`).\n `gradientUnits="userSpaceOnUse"` uses the coordinates as-is instead.\n- Each gradient holds **`<Stop offset stopColor stopOpacity>`** children.\n `offset` accepts either `0.5` or `"50%"`.\n\nDiffers from react-native-svg: there is no `gradientTransform`, and no\n`spreadMethod` beyond the default pad behavior.',
1316
+ },
1317
+ {
1318
+ doc: "docs/reference/svg.md",
1319
+ heading: "Animated values",
1320
+ text: "The numeric props above — shape geometry, `opacity`, `strokeWidth`,\n`strokeDashoffset` — accept an `Animated.Value` or interpolation in place of\na plain number. A tick mutates the widget's paint state directly and calls\n`queueDraw()`, the same bypass-React pattern `Animated.View` uses for\n`transform`, on its own invalidation channel, since none of this touches\nYoga.\n\n`G`'s `transform` string and `Path`'s `d` / `Polygon`/`Polyline`'s `points`\nare not Animated-aware — they are strings, not numbers.",
1321
+ },
1322
+ {
1323
+ doc: "docs/reference/svg.md",
1324
+ heading: "Differs from react-native-svg",
1325
+ text: "The shape set here — `Path`, `Rect`, `Circle`, `Ellipse`, `Line`, `Polygon`,\n`Polyline`, `G`, gradients — covers icons, charts and indicators, which is\nthe overwhelming majority of real SVG usage. The following are not part of\nthe surface:\n\n- No `SvgXml` / `SvgUri` — rasterizing an arbitrary SVG string or URI at\n runtime. Loading `.svg` **files** is a different, already-covered\n mechanism: `Image` loads them today, through its own rasterized-image\n path rather than this vector widget tree. See the components reference\n for that entry — it is not repeated here.\n- No `<Text>` / `<TSpan>` / `<TextPath>` — text laid out along or inside a\n path.\n- No `<Mask>`, `<ClipPath>`, SVG filters, `<Use>`, `<Symbol>`, or\n `<Pattern>`.\n\nNone of these have a real consumer yet in this platform's own apps.",
1326
+ },
1327
+ {
1328
+ doc: "docs/reference/components/index.md",
1329
+ heading: "Layout, paint and hit-testing",
1330
+ text: "A few rules apply across every component in this section rather than to one\nof them:\n\n![The gallery's Transforms section: a translated box painting over a neighbor without moving it, plus rotate/scale composed left to right.](../../shots/gallery/transforms.png)\n\n- **`zIndex` orders paint and picking, per sibling group.** GTK4 has no\n z-order property, so the container widget provides it: children are\n allocated in their Yoga order and painted (snapshotted) in `zIndex` order,\n and a widget a higher-painting sibling covers declines to be hit-tested, so\n input follows the pixels. Layout itself is untouched — only the paint pass\n is sorted. The rules match RN, each checked rather than assumed: `zIndex`\n applies whatever the component's `position` is (CSS requires a non-`static`\n position; neither RN nor this platform does); equal values keep document\n order (the sort is stable); `undefined` behaves as `0`, and negative values\n are legal and paint below untagged siblings; and the ordering is **scoped to\n one sibling group** — it creates no stacking context that escapes the\n parent, so a child can never paint above its parent's own siblings. That\n last rule is what to design around: to lift a dragged item over a drop\n target, put the `zIndex` on the dragged item's row, exactly as on iOS and\n Android. `Animated.View` and `useAnimatedStyle` reorder on the same terms as\n `opacity` — one widget write, no Yoga pass.\n\n One divergence from RN: an interactive native leaf inside a covered\n sibling — a `TextInput`, a `Switch`, a `ScrollView` viewport, a raw GTK\n widget in a slot — still receives a press even where a raised view visually\n covers it, because GTK's per-point hit test is consulted after a widget's\n children regardless of paint order. `Text` and `Image` do not have a press\n prop of their own, so while something is raised above them they are\n excluded from hit-testing and the press reaches their nearest `View`\n instead — which is also why a `pointerEvents: \"box-none\"` `View` whose only\n child is `Text` lets a press fall through to whatever is behind it, while a\n sibling in that same container stays raised.\n\n- **`transform` is paint-only, like RN.** `translateX`/`translateY`, `scale`,\n `scaleX`, `scaleY` and `rotate`/`rotateZ` apply to any component's style,\n not just `Animated.View`; the array composes left to right, as in RN and\n CSS, and the origin is always the component's own center. A transformed\n child draws past its container and over siblings — later siblings stay on\n top unless `zIndex` says otherwise — without moving any ancestor, and input\n follows the transform: a rotated view is clickable in its rotated shape,\n unless a container's `overflow: \"hidden\"` clips it at the edge exactly as it\n clips an untransformed child. Not supported: 3D transforms (`rotateX`,\n `rotateY`, `perspective`), `skewX`/`skewY`, `matrix`, and `transformOrigin`\n (the origin is always centered).\n\n- **Animations never auto-stop.** The desktop's own \"reduce animations\"\n preference is not applied automatically — GTK-side animations stay on to\n match `Animated`, which runs on its own timers regardless of that setting.\n Honoring reduced motion is an app-level opt-in, exactly as it is in RN.\n\nSee [Styling](../styling.md) for the full style-property reference (what\nreaches Yoga, what reaches GTK CSS, and what `overflow` does at the boundary\nbetween the two).",
1331
+ },
1332
+ {
1333
+ doc: "docs/architecture/overview.md",
1334
+ heading: "The path from JSX to a window",
1335
+ text: "A `View`, a `Text`, a `Pressable` are ordinary React function components —\nthis package has no reconciler of its own. Each one renders a gtkx host\nelement (a `GtkBox`, in `View`'s case) that gtkx's own reconciler mounts as a\nreal GTK widget through the FFI: a widget call is a synchronous, in-process\ncall, with no serialization step between it and the code that made it. That\nremoves the \"bridge tax\" that shaped classic React Native's architecture —\nthere is no batched JSON channel to a separate UI thread to cross.\n\nPosition and size are a second, independent handoff. Every RN-shaped\ncontainer widget installs a custom `Gtk.LayoutManager` subclass, registered\nfrom JS, whose `measure()` and `allocate()` vfuncs do nothing but ask this\npackage's own layout engine for a number and a set of child rectangles. GTK's\nlayout cycle is not synchronized with Yoga after the fact — for these\nwidgets, Yoga computes the layout GTK's allocation pass performs. The engine\nthat does this, the shadow tree it keeps, and the style split that feeds it\nare the subject of [Layout and styling](layout-and-styling).",
1336
+ },
1337
+ {
1338
+ doc: "docs/architecture/overview.md",
1339
+ heading: "Three subpaths beneath the portable surface",
1340
+ text: 'Everything reachable from plain `"react-native"` is portable. Underneath it,\nthree subpaths give you the platform itself, with nothing filtered out:\n\n```\nyour app\n ├── react-native portable components\n ├── react-native-gtkx/navigation react-navigation adapter (optional)\n ├── react-native-gtkx/common what this package wrote itself\n ├── react-native-gtkx/adw libadwaita widgets, bound directly\n └── react-native-gtkx/gtk GTK widgets, bound directly\n```\n\nThree rules make the rest of this page easy to reason about:\n\n1. **The import says what you\'re opting into.** Anything from\n `react-native-gtkx/gtk` or `react-native-gtkx/adw` is Linux-only, which\n shows up in review as a decision, not an accident.\n2. **A prefix tells you whose widget it is.** `AdwHeaderBar`, `GtkButton`,\n `AdwNavigationView` — that IS the widget, bound by gtkx. No prefix —\n `NavigationStack`, `SlotContent`, `Widget` — means this package wrote it.\n A wrapper of ours never makes the underlying widget unreachable.\n3. **None of this knows about react-navigation.** No router is involved and\n none is required. `react-native-gtkx/navigation` is a thin adapter built\n on these primitives, the same way `@react-navigation/native-stack` is\n built on `react-native-screens` — an app can skip the adapter entirely\n and drive an `Adw.NavigationView` from its own state. See\n [Window, navigation, and settings](integration) for that adapter and for\n everything the three subpaths expose beyond a single widget.',
1341
+ },
1342
+ {
1343
+ doc: "docs/architecture/overview.md",
1344
+ heading: "The widget surface: wrapped, raw, and auxiliary",
1345
+ text: 'Every `Gtk.Widget` and `Adw.Widget` subclass gtkx binds is exported — 86 GTK\nwidgets and 46 Adwaita widgets at present, from `GtkBox` and `GtkButton` to\n`GtkColumnView` and `AdwToolbarView`. The list is generated, not hand-picked:\n`scripts/generate-widget-surface.ts` classifies gtkx\'s full binding by real\nGObject inheritance, and the classification is committed\n(`scripts/widget-surface/classification.json`) so it stays exact between\ngtkx upgrades — re-run the generator after one to pick up new widgets; it\ndiffs against its own previous output.\n\nMost of that surface is **wrapped**: it keeps every prop gtkx binds and gains\n`style`/`onLayout`, exactly like any other React Native component.\n\n```tsx\n<GtkEntry\n style={{ flex: 1 }}\n placeholderText="Filter"\n/>\n<GtkButton\n style={{ width: 72, backgroundColor: "#3584e4", borderRadius: 6 }}\n label="Go"\n/>\n```\n\nThe entry flexes, the button takes its own width and color — the layout half\nof the style drives Yoga, the visual half becomes a GTK CSS class **on the\nwidget itself**, so the button really is blue rather than a blue box sitting\nbehind one.\n\nTwo families are exported **raw** instead, because a wrapper box around them\nwould be invalid GTK rather than a convenience:\n\n- **Toplevels** — everything implementing `GtkRoot`: `GtkWindow` and every\n `Gtk*Dialog`, `GtkApplicationWindow`, `GtkAssistant`, `GtkShortcutsWindow`\n and their Adwaita counterparts (`AdwWindow`, `AdwApplicationWindow`,\n `AdwAboutWindow`, `AdwMessageDialog`, `AdwPreferencesWindow`), plus\n `GtkDragIcon` — which derives `Gtk.Widget` directly and is a toplevel all\n the same, which is why the rule is written against the `GtkRoot`\n capability rather than against `Gtk.Window` as one familiar instance of\n it. `GtkPopover` sits on the other side of that line — a `GtkNative` but\n not a `GtkRoot`, parented with `gtk_popover_set_parent` — and stays\n wrapped.\n- **Child-only widgets** — valid solely as the direct child of one specific\n parent. `GtkListBoxRow` and `GtkFlowBoxChild` (plus everything deriving\n them — every Adwaita preferences row, `AdwActionRow` included) are caught\n mechanically, by inheritance. `AdwNavigationPage` and `AdwPreferencesPage`\n derive `Gtk.Widget` directly with no shared base to catch them the same\n way, so they\'re a small, doc-verified denylist instead — see\n `scripts/widget-surface/classify.ts`.\n\nEvery raw export above is still exported, by name, from `react-native-gtkx/gtk`\nor `/adw`, exactly as gtkx binds it — reach the widget with a `ref` where you\nneed one directly. `GtkGestureClick` is a third, simpler case: an event\ncontroller, not a widget at all, so it was never a candidate for wrapping.\n\nA further set of exports are not `Gtk.Widget`/`Adw.Widget` subclasses at all,\nso the generator never sees them either: actions and menus (`GSimpleAction`,\n`GMenu`), a responsive breakpoint (`AdwBreakpoint`, detailed in\n[Layout and styling](layout-and-styling)), one option of an `AdwToggleGroup`\n(`AdwToggle`), the two leaf elements an `AdwShortcutsDialog` is built from,\na text buffer and an adjustment (`GtkTextBuffer`, `GtkAdjustment`), keyboard\nshortcuts (`GtkShortcut`, `GtkShortcutController`), and the two drag-and-drop\ncontrollers (`GtkDragSource`, `GtkDropTarget`):\n\n```tsx\n<GtkApplicationWindow\n actions={\n <GSimpleAction\n name="new"\n onActivate={onNew}\n />\n }\n breakpoints={\n <AdwBreakpoint\n condition={Adw.BreakpointCondition.parse("max-width: 500sp")}\n onApply={() => setCollapsed(true)}\n onUnapply={() => setCollapsed(false)}\n />\n }\n/>\n```',
1346
+ },
1347
+ {
1348
+ doc: "docs/architecture/overview.md",
1349
+ heading: "`react-native-gtkx/common`: what has no upstream counterpart",
1350
+ text: "Nothing in this subpath carries an `Adw`/`Gtk` prefix, because none of it is\na binding — it's the plumbing between the two worlds:\n\n- **`Icon`** — a _named_ icon resolved against the desktop icon theme at\n paint time, not a bundled asset like RN's `Image`. It recolors itself\n with the label color and follows the user's theme, which nothing in\n `Image`'s contract can express, behind the same shape RN apps already use\n (`<Icon name size />`).\n- **`SlotContent` / `IntrinsicContent`** — the boundary that lets React\n Native content live inside a GTK widget's slot or content area. Detailed\n in [Layout and styling](layout-and-styling), which is where the boundary\n actually matters.\n- **`Widget` / `wrapReactNative` / `useWidgetLayout`** — the reverse\n direction: giving a raw GTK widget a place in React Native layout. Also in\n [Layout and styling](layout-and-styling).\n- **`NavigationStack` / `NavigationStackPage`** — a declarative layer over\n `Adw.NavigationView`, which is imperative (`push`/`pop`/`pop_to_tag`) where\n React is not. Detailed in\n [Window, navigation, and settings](integration).\n\nThis platform does not re-implement Adwaita chrome in React Native — reach\nfor `AdwActionRow` and friends from `react-native-gtkx/adw`, inside a\n`GtkListBox` with `cssClasses={[\"boxed-list\"]}` (see the\n[Reference](../reference) for the full row family). The style layer's\n`boxShadow`, `outline*` and `textDecorationLine` properties exist precisely\nso that an Adwaita-looking list stays expressible in a plain `StyleSheet`\nwhen you do want to build one by hand — the frame is a three-part\n`box-shadow` rather than a border, the focus ring an `outline`, which takes\nno layout space. Drag-to-reorder goes through one module,\n`react-native-gtkx/dnd` (see the Reference) — a `Droppable` around a\n`Draggable` per row inside one `DropProvider` — rather than a bespoke\nreorder prop.",
1351
+ },
1352
+ {
1353
+ doc: "docs/architecture/overview.md",
1354
+ heading: "Namespaces",
1355
+ text: "`Adw`, `Gdk`, `Gio`, `Gtk` and `Pango` are exported as values from both\n`react-native-gtkx/gtk` and `/adw`, because code needs both the runtime enums\nand the types:\n\n```tsx\nconst scroller = <GtkScrolledWindow hscrollbarPolicy={Gtk.PolicyType.NEVER} />\nconst viewRef = useRef<Adw.NavigationView | null>(null)\n```",
1356
+ },
1357
+ {
1358
+ doc: "docs/architecture/overview.md",
1359
+ heading: "Wrapping a widget this package hasn't caught up to yet",
1360
+ text: "`scripts/generate-widget-surface.ts` covers every `Gtk.Widget`/`Adw.Widget`\nsubclass gtkx binds as of its last run, but a gtkx release can add a widget\nbefore the generator has been re-run for it, and a non-widget GI class (an\nevent controller, a filter, an adjustment) was never a generator candidate in\nthe first place. `wrapReactNative` reaches either without waiting — it's\ngeneric, so the widget's own prop types survive:\n\n```tsx\nimport { GtkPopover } from \"@gtkx/jsx/gtk\"\nimport { wrapReactNative } from \"react-native-gtkx/common\"\n\nconst Popover = wrapReactNative(GtkPopover)\n// <Popover style={{ width: 240 }} autohide … /> — `autohide` still typed\n```\n\nThat's the same mechanism the generated surface itself uses under the hood,\napplied by hand. Two lower-level forms exist for cases even that doesn't fit:\n`<Widget style={…}>` wraps an element already in hand, and\n`useWidgetLayout(ref, { style })` attaches layout to a widget whose ref you\nalready own, with no wrapper component at all.",
1361
+ },
1362
+ {
1363
+ doc: "docs/architecture/overview.md",
1364
+ heading: "The escape hatch",
1365
+ text: "If something is still missing, reach the widget directly — every wrapper\nhere forwards its `ref` to the real GObject:\n\n```tsx\nconst viewRef = useRef<Adw.NavigationView | null>(null)\n<NavigationStack ref={viewRef} stack={stack}>…</NavigationStack>\n// viewRef.current is the real Adw.NavigationView\n```\n\nThere is deliberately no wall. A missing convenience should cost one line,\nnot a fork.\n\n---\n\nMeasured numbers behind these decisions (the Yoga/GTK feasibility spike,\nframe-budget studies, the navigation research) live in `docs/research/` —\nrepo-only working notes, not published here. The standing gtkx upstream\nagenda is `docs/upstream-gtkx.md`, and every workaround the bridge carries\nis cataloged in `docs/gtkx-1.2-notes.md`.",
1182
1366
  },
1183
1367
  {
1184
- doc: "docs/gestures.md",
1368
+ doc: "docs/architecture/overview.md",
1369
+ heading: "Related",
1370
+ text: "- [Reference](../reference) — the full component and API surface, GTK/Adw\n by badge.\n- [Layout and styling](layout-and-styling) — the Yoga shadow tree, the\n layout/visual style split, and the two ways to react to a resize.\n- [Window, navigation, and settings](integration) — `NavigationStack`,\n window actions and controllers, `GSettings`.\n- [Gestures](gestures) — the responder system and `PanResponder` on GTK\n event controllers.",
1371
+ },
1372
+ {
1373
+ doc: "docs/architecture/layout-and-styling.md",
1374
+ heading: "One Yoga engine per layout root",
1375
+ text: "A `LayoutEngine` owns one Yoga tree and batches every mutation — a style\nchange, a tree edit, a measurement invalidation — into a single Yoga\n`calculateLayout` pass per microtask, however many components touched the\ntree before the pass runs. After that pass, committing widget rectangles is\n**incremental**: the engine walks only the paths a mutation could have\nchanged, driven by two signals together —\n\n- Yoga's own per-node `hasNewLayout` flag, set on every node Yoga actually\n re-laid out. It catches what a dirty set alone can't know: changing one\n child re-lays out its _following siblings_ (they shift) and any ancestor\n whose size followed, while an untouched subtree keeps its cached,\n parent-relative layout even when its container moved.\n- the engine's own dirty set — which node each mutation actually came from —\n which catches what Yoga's flag doesn't imply: a re-measured leaf whose\n rectangle came out identical still has to recommit, because measuring it\n reset its own widget size request.\n\nWidget moves are committed first for the whole pass, then `onLayout`\ncallbacks fire in a second pass over only the entries whose rect changed —\nmatching React Native's own two-phase order.",
1376
+ },
1377
+ {
1378
+ doc: "docs/architecture/layout-and-styling.md",
1379
+ heading: "GTK's allocation cycle IS the Yoga pass",
1380
+ text: "Every RN-shaped container widget (`View`'s `GtkBox`, and anything wrapped\nthrough `Widget`/`wrapReactNative`) runs a custom `Gtk.LayoutManager`\nsubclass, registered from JS, that does nothing but delegate: `measure()`\nreturns whatever the engine already computed for that node, and\n`allocate()` hands the container's final size to the engine, which places\nevery child at its computed rectangle synchronously, inside GTK's own\nallocation pass. GTK never queries children for their own size preferences\nthrough this path — Yoga already decided, and GTK is told, not asked. That\nis what removes the layout conflicts a naive integration would hit: a\nwindow's minimum-size ratchet, overflow children inflating their ancestors,\nwidget minimums pushing rectangles around.",
1381
+ },
1382
+ {
1383
+ doc: "docs/architecture/layout-and-styling.md",
1384
+ heading: "Three flavors of layout root",
1385
+ text: "A layout root is where a `LayoutEngine` is created. There are three, and the\ndifference is which side reports size to which:\n\n- **The window root**, created once by `AppRegistry.runApplication`. In the\n ordinary case it adopts GTK's own window allocation as its Yoga viewport —\n the window decides the size, layout fills it.\n- **`SlotContent`** (`NestedRoot`) — a full, independent Yoga engine mounted\n inside _any_ GTK container slot: an `Adw.NavigationPage`'s content, a\n toolbar view's body, a future container nobody has written yet. It follows\n the slot's own allocation exactly like the window root follows the\n window — the slot decides the size.\n- **`IntrinsicContent`** (`IntrinsicRoot`) — the other direction: this root's\n own Yoga-computed content size becomes _its_ size request to GTK, so a\n `HeaderBar` slot or a sidebar row can ask \"how big are you?\" and get an\n answer built from real React Native content. Measuring runs a speculative,\n uncommitted Yoga pass first (honoring GTK's width-for-height style\n constraint), and the allocation pass that follows recomputes at the real\n size and commits it.\n\nUse `SlotContent` for a page body, a pane, a dialog body — anything that\nshould fill the rectangle it's given. Use `IntrinsicContent` for a HeaderBar\nslot, a toolbar area, a list row — anything that should be sized by what it\nholds. `createSidebarNavigator`'s `sidebarRow` screen option wraps its\ncontent in exactly `IntrinsicContent`, because a sidebar row is sized by\nwhat it holds, not stretched to fill the list.",
1386
+ },
1387
+ {
1388
+ doc: "docs/architecture/layout-and-styling.md",
1389
+ heading: "Why the boundary matters",
1390
+ text: "A GTK widget hands out rectangles two ways: as ordinary children (a content\narea) and as slots — properties that take a widget, `titleWidget={…}`,\n`sheet={…}`. Which way a given area arrives is gtkx's own business, and it\nmoves between gtkx releases; it has never had anything to do with layout.\nBoth are GTK's territory, and both need the same thing on the way in: the\nenclosing React Native layout root is cleared, so a widget lands bare, and\nanything that should be React Native content again has to bring its own\nroot — one of these two:",
1391
+ },
1392
+ {
1393
+ doc: "docs/architecture/layout-and-styling.md",
1394
+ heading: "React Native content inside GTK slots",
1395
+ text: "| Export | Sizing | Use for |\n| ------------------ | ---------------------------- | ------------------------------------------------ |\n| `SlotContent` | fills the slot | a page body, a pane, a dialog body |\n| `IntrinsicContent` | sized by its own Yoga layout | an AdwHeaderBar slot, a toolbar area, a list row |\n\nForget the wrapper, and the failure is not a wrong-looking window — it's\ncontent silently laid out against the _wrong_ rectangle. Without a root,\ncontent dropped into a widget's slot or child position would join the\n_enclosing_ Yoga tree, measured against the window's viewport, while GTK\nhands the widget only its own rectangle: laid out against one box, drawn in\nanother, quietly stealing space from a tree it was never in. The platform\ncatches this instead of letting it happen silently — every element-valued\nprop a wrapped widget is given, and its children, are put behind a boundary\nthat clears the layout root and remembers where the content was headed, so\nthe first read of a Yoga hook downstream throws a message naming the exact\nwidget and slot (\"`AdwBottomSheet`'s `sheet` slot\") and which of\n`SlotContent`/`IntrinsicContent` to wrap it in.\n\nWhich of the two is right cannot be inferred, and one widget proves why:\n`AdwBottomSheet` alone FILLS its content child but HUGS both `sheet` (a\nbottom sheet rises to the height of its own contents) and `bottomBar`. One\nwidget, three content areas, two answers, nothing in the name or the GI type\nto tell them apart — the answer lives in the widget's own layout code, not\nin a rule this platform could apply mechanically.\n\n```tsx\n<AdwBottomSheet\n style={{ flex: 1 }}\n sheet={\n <IntrinsicContent>\n <View style={{ padding: 20, gap: 10 }}>…</View>\n </IntrinsicContent>\n }\n bottomBar={\n <IntrinsicContent>\n <View style={{ flexDirection: \"row\", gap: 8 }}>…</View>\n </IntrinsicContent>\n }\n>\n <SlotContent>\n <View style={{ flex: 1, justifyContent: \"center\" }}>…</View>\n </SlotContent>\n</AdwBottomSheet>\n```\n\nNote the two independent sizes here: `style={{ flex: 1 }}` on `AdwBottomSheet`\nitself is the _widget's_ size in the surrounding React Native layout (a\nwrapped widget is a Yoga leaf at its own natural size until a style says\notherwise); the wrapper inside each content area sizes the _content_ within\nthe rectangle that widget then hands out.",
1396
+ },
1397
+ {
1398
+ doc: "docs/architecture/layout-and-styling.md",
1399
+ heading: "Giving a raw GTK widget a place in Yoga's tree",
1400
+ text: 'The reverse bridge — a GTK widget that should participate in React Native\nlayout rather than sit in a slot — is `Widget`, `wrapReactNative`, and\n`useWidgetLayout`. All three do the same thing: give the widget a Yoga leaf,\napply the layout half of a style to it, and — the part that matters — measure\nthe widget\'s own natural size, so it lands at the size the GTK theme wants\nrather than collapsing to zero.\n\n```tsx\n<View style={{ flexDirection: "row", gap: 8, padding: 12 }}>\n <Widget style={{ flex: 1 }}>\n <GtkEntry placeholderText="Search" />\n </Widget>\n <Widget>\n <GtkButton iconName="edit-find-symbolic" />\n </Widget>\n</View>\n```\n\n`wrapReactNative` additionally detects, at render time, whether there\'s a\nYoga tree to join at all. **Outside React Native layout it steps aside**: the\nsame `GtkButton` dropped into an `AdwHeaderBar`\'s `start` or an\n`AdwToolbarView`\'s `topBar` — where there is no enclosing root — renders as\nthe bare widget, with `style`/`onLayout` dropped rather than forwarded to a\nGObject property that doesn\'t exist. One exported symbol, both worlds, no\nflag to remember.',
1401
+ },
1402
+ {
1403
+ doc: "docs/architecture/layout-and-styling.md",
1404
+ heading: "The style split",
1405
+ text: "A flattened style is partitioned into three disjoint buckets, each consumed\nby exactly one part of the pipeline, and the split is exhaustive by\nconstruction — adding a new style key without classifying it fails\ncompilation, not a runtime check:\n\n- **Layout properties** (`flex`, `padding`, `gap`, `position`, and the rest\n of Yoga's own vocabulary) drive the Yoga node directly.\n- **Visual properties** (`backgroundColor`, `borderRadius`, `boxShadow`,\n `outline*`, `opacity`, the font properties, `transform`) compile to a GTK\n CSS class applied to the widget itself — not a wrapper around it — except\n `transform`, which is applied as the child's allocation transform rather\n than through CSS, and `textAlign`/`textDecorationLine`, which Pango\n carries and `Text` applies directly.\n- **Behavioral properties** (`pointerEvents`, `zIndex`) belong to neither\n Yoga nor CSS and are consumed silently by the component that owns the\n behavior: `pointerEvents` maps onto GTK's own hit-testing (`can-target`,\n and a `contains()` override for `box-none`/`box-only`), and `zIndex`\n becomes the enclosing container's paint and pick order. GTK4 CSS has no\n `overflow` property either, so `overflow` is the one visual-shaped\n property applied as a direct widget call rather than a class — it also\n has to reach Yoga, since Yoga needs it while measuring, so it is the one\n style property both halves of the pipeline read.\n\nAn unrecognized property warns once, by name, and is dropped — this is what\ncatches a typo or an unimplemented RN style property before it silently does\nnothing.",
1406
+ },
1407
+ {
1408
+ doc: "docs/architecture/layout-and-styling.md",
1409
+ heading: "Two ways to react to size",
1410
+ text: "Two mechanisms answer two different questions, and neither replaces the\nother.\n\n**\"Render different content at different widths\"** is `useWindowDimensions`\n— portable, and already how React Native answers this everywhere else. A\nresize triggers a React render, the component reads the new width, and\nreturns different JSX. This is the only tool for anything that changes\n_what_ is rendered: swapping a filter bar for a compact one, hiding a\ncolumn, changing text.\n\n**\"Flip a widget property natively at a threshold, with no render at all\"**\nis `AdwBreakpoint` + `AdwBreakpointBin`. `Adw.Breakpoint` is a condition — a\nsize or aspect-ratio threshold — plus a set of property setters: when the\ncondition starts holding, each setter writes its value onto its target\nobject's property directly, through GObject, inside GTK's own allocation\npass; when it stops holding, the setter restores whatever value the property\nheld before. No React commit, no Yoga pass, no JS callback runs for the flip\nitself — a resize costs nothing beyond what GTK's layout was already doing.\n\n`Adw.Breakpoint` is not a widget — its prototype chain bottoms out at plain\n`GObject.Object`, not `Gtk.Widget` — so it's exported raw, the same way\n`GtkGestureClick` is: running it through `wrapReactNative` would hand a Yoga\nnode to something that isn't a rectangle. `AdwBreakpointBin` **is** a real\nwidget — a container that scopes breakpoints to its own child subtree\ninstead of a whole window — and is wrapped normally. A breakpoint's setters\nmay only target widgets _inside_ the bin they're attached to, never the bin\nitself:\n\n```tsx\nconst splitViewRef = useRef<Adw.NavigationSplitView | null>(null)\nconst breakpointRef = useRef<Adw.Breakpoint | null>(null)\n\nuseEffect(() => {\n if (!splitViewRef.current || !breakpointRef.current) return\n const collapsed = new GObject.Value()\n collapsed.init(GObject.typeFromName(\"gboolean\"))\n collapsed.setBoolean(true)\n breakpointRef.current.addSetter(splitViewRef.current, \"collapsed\", collapsed)\n}, [])\n\n<AdwBreakpointBin\n breakpoints={\n <AdwBreakpoint\n ref={breakpointRef}\n condition={Adw.BreakpointCondition.newLength(\n Adw.BreakpointConditionLengthType.MAX_WIDTH,\n 500,\n Adw.LengthUnit.SP,\n )}\n />\n }\n>\n <AdwNavigationSplitView ref={splitViewRef} …>…</AdwNavigationSplitView>\n</AdwBreakpointBin>\n```\n\n`addSetter` wants a genuine, boxed `GObject.Value` — a bare JS `true` fails a\n`G_IS_VALUE` assertion on the native side rather than silently coercing.\n`createSidebarNavigator`'s own `collapseWidth` option is built on exactly\nthis pair; reading `collapsed`/`showContent` back is a plain native property\nread through the same ref, not React state, so neither the flip nor a read\nof it costs a render.\n\nNo `useBreakpoint(condition) → boolean` hook exists. It would return a flag\nto JS and re-render on every crossing — a second name for\n`useWindowDimensions`, with none of the native setter's value. Reach for\n`useWindowDimensions` when the thing that should change is your component's\nJSX; reach for `AdwBreakpoint` only when the thing that should change is a\nwidget property GTK itself owns, and the change should cost nothing.\n\nOne limitation, stated as what it is rather than found along the way: under\nthis project's headless test compositor (sway), `AdwBreakpoint`'s\n`onApply`/`onUnapply` do not fire, even past a genuine resize past the\ncondition's threshold; they fire exactly as documented in a real GNOME\nsession. Treat this as a test-environment limitation, not a runtime defect.",
1411
+ },
1412
+ {
1413
+ doc: "docs/architecture/layout-and-styling.md",
1414
+ heading: "Related",
1415
+ text: "- [Overview](overview) — the widget surface this mechanism serves, and\n where `Widget`/`wrapReactNative`/`SlotContent`/`IntrinsicContent` are\n exported from.\n- [Window, navigation, and settings](integration) — `createSidebarNavigator`'s\n `collapseWidth`, built on the breakpoint mechanism above.",
1416
+ },
1417
+ {
1418
+ doc: "docs/architecture/integration.md",
1419
+ heading: "Navigation without a router",
1420
+ text: "`NavigationStack` and `NavigationStackPage` are the two components this\npackage wraps a raw `Adw.NavigationView` in, because it's imperative\n(`push`/`pop`/`pop_to_tag`) where React is not:",
1421
+ },
1422
+ {
1423
+ doc: "docs/architecture/integration.md",
1424
+ heading: "Declarative primitives",
1425
+ text: '| Export | What it is |\n| --------------------- | ------------------------------------------------------ |\n| `NavigationStack` | `Adw.NavigationView` driven by a `stack` array of tags |\n| `NavigationStackPage` | one page of that stack, identified by `tag` |\n\nThey inherit every prop of the underlying widget and only add to it, so\nanything settable on `Adw.NavigationPage` is settable on `NavigationStackPage`\ntoo. The navigation state is an ordinary array of tags — change the array,\nthe widget animates:\n\n```tsx\nconst App = () => {\n const [stack, setStack] = useState(["home"])\n\n return (\n <NavigationStack\n stack={stack}\n // The Adwaita back button, Escape, the back gesture and the\n // back-history menu all arrive here. Follow them in your own state.\n onPopped={(tag) => setStack((s) => s.filter((entry) => entry !== tag))}\n >\n <NavigationStackPage\n tag="home"\n title="Home"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <Pressable onPress={() => setStack((s) => [...s, "detail"])}>\n <Text>Open detail</Text>\n </Pressable>\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n\n <NavigationStackPage\n tag="detail"\n title="Detail"\n >\n <AdwToolbarView topBar={<AdwHeaderBar />}>\n <SlotContent>\n <View />\n </SlotContent>\n </AdwToolbarView>\n </NavigationStackPage>\n </NavigationStack>\n )\n}\n```\n\nPages not listed in `stack` are still accepted as children and simply aren\'t\nshown, so a router can hand over every screen it owns at once. As an app\'s\nroot — where GTK allocates it directly — `NavigationStack` needs nothing\nelse; nested anywhere inside a React Native layout it needs wrapping in\n`Widget`, because the component renders a raw `Adw.NavigationView`, which has\nno Yoga node of its own.\n\n`NavigationStack` inherits every prop of `Adw.NavigationView` and adds:\n\n| Prop | Meaning |\n| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stack` | ordered page tags, root first — the navigation state |\n| `animateTransitions` | forwarded to `Adw.NavigationView`\'s own property; default true. GTK has one transition style, so this is on/off |\n| `onPopped(tag)` | the widget popped by itself — not called for pops caused by changing `stack` |\n| `onPageClosed(tag)` | a closing page finished animating out and left the tree |\n| `onTransitionStart()` / `onTransitionEnd()` | a push/pop/replace began or finished, the latter driven by the transitioning page\'s own `shown`/`hidden` signal |\n| `transitionDuration` | ms, default 400 — a fallback window for retention and the callbacks above, used only when a page\'s transition signal never arrives; not a measurement of real transition length |\n| `ref` | the `Adw.NavigationView` itself |\n\n**Exit animations are handled for you.** When a tag leaves `stack`, the\nwidget still animates the page out — `NavigationStack` keeps a snapshot of\nthat page until its `hidden` signal, with a timer fallback for the two cases\nwhere that signal never arrives on its own (a compositor that never emits\nit, and a page skipped over entirely by a multi-hop pop), so nothing keeps\nrendering a page it already considers gone.\n\nReact Native content in native chrome is the same `IntrinsicContent`\nboundary the layout page describes, used against a HeaderBar slot:\n\n```tsx\n<AdwHeaderBar\n start={\n <IntrinsicContent>\n <Text>{stack.length} deep</Text>\n </IntrinsicContent>\n }\n end={[\n <GtkButton\n key="home"\n iconName="go-home-symbolic"\n onClicked={reset}\n />,\n ]}\n/>\n```',
1426
+ },
1427
+ {
1428
+ doc: "docs/architecture/integration.md",
1429
+ heading: "Mixing with react-navigation",
1430
+ text: "`react-native-gtkx/navigation` composes with everything above, because the\nnavigator is built on these same primitives — it is a convenience, not a\nceiling. Use it for an app's overall structure and drop to\n`react-native-gtkx/gtk`/`/adw` wherever a screen needs a widget the\nnavigator's options don't cover: a raw `GtkButton` in `headerButtons`, a\n`GtkListBox` inside a screen. Keep portable code portable with a `.linux.tsx`\nplatform extension or `Platform.select({ linux: … })` — an option a platform\ndoesn't understand is simply ignored, and in development the navigator warns\nwith the screen and option name rather than swallowing it silently.",
1431
+ },
1432
+ {
1433
+ doc: "docs/architecture/integration.md",
1434
+ heading: "Reaching the window and the application",
1435
+ text: '`useParentWindow` (the nearest `Gtk.Window` ancestor), `useApplication` (the\n`Adw.Application` — `.sendNotification(id, notification)` is the common\nreason to reach it), and `quit` (the same function `AppRegistry` wires to a\nwindow\'s own close button) let already-mounted code reach back into objects\n`AppRegistry.runApplication` already built, rather than construct them:\n\n```tsx\nconst window = useParentWindow()\nuseBindSetting({\n schema,\n key: "window-width",\n object: window,\n property: "defaultWidth",\n})\n```',
1436
+ },
1437
+ {
1438
+ doc: "docs/architecture/integration.md",
1439
+ heading: "Actions and shortcuts, declared in the tree",
1440
+ text: "`WindowActions`, `ApplicationActions` and `WindowControllers` register their\nchildren on the window or the application **from wherever they're written in\nthe component tree**. They render nothing where they sit — they're portals\nin React's own sense: the children stay part of the tree at that position,\nkeeping the context, state and effects they'd have there, while the\nregistration itself lands on the window or application object.\n\n```tsx\nconst NewTaskAction = () => {\n const { addTask } = useStore() // an ordinary React context store\n return (\n <WindowActions>\n <GSimpleAction\n name=\"new\"\n onActivate={() => addTask()}\n />\n </WindowActions>\n )\n}\n```\n\nThat's `win.new` — what a HeaderBar button's `actionName`, a `GMenu` item and\nan `actionAccels` entry all target. `ApplicationActions` is the same\ncomponent against the application's action map (`app.*`); the two prefixes\nare not interchangeable — a `Gio.Notification`'s action button can only ever\nactivate an application action, and an application action outlives any one\nwindow. `WindowControllers` takes `Gtk.EventController` children; a\n`GtkShortcutController` with `scope={Gtk.ShortcutScope.GLOBAL}` is the\nreason it exists.\n\n**Reach for these, not `runApplication`'s `applicationActions`/\n`windowActions`/`windowControllers` options.** Those options build their\nchildren as props of the window `AppRegistry` creates, making them _siblings_\nof the app tree rather than descendants — no provider inside the app sits\nabove them, so an action declared there can't read a React context at all.\nThe components fix all three things the options can't:\n\n- **context works**, because the declaration is a descendant of its own\n provider;\n- **registration is dynamic** — added on mount, removed on unmount, so one\n screen can own actions for exactly its own lifetime;\n- **it composes** — two unrelated subtrees each declare their own without\n meeting in one shared options object.\n\n`actionAccels` is not deprecated and stays a `runApplication` option: it's a\nflat name→keys table with no children and nothing to read from context, and\nit's deliberately process-wide — naming an action that isn't registered\nright now simply does nothing. A shortcut that should come and go with a\nscreen is a `GtkShortcutController` inside `WindowControllers` instead.\n\n**Two components, not one, because the two targets are different GObject\ninterfaces with different duplicate semantics.** Actions land on the window\nas a `Gio.ActionMap` (`addAction`/`removeAction`, keyed by name); controllers\nland on it as a `Gtk.Widget` (`addController`/`removeController`, keyed by\nthe controller object itself). One component sorting its children by type\nwould fail silently on a wrong child; two fail at the type level instead.\n\nA duplicated action name goes to the **first** declaration; a second one is\nignored, with a development warning naming it. This isn't an arbitrary\nchoice between first and last: `Gio.ActionMap` is name-keyed at both ends —\n`addAction` silently replaces a same-named action, and `removeAction` takes a\nname, not the action object. Under \"last wins,\" the first of two same-named\ndeclarations to unmount would remove whatever currently answers to that\nname, leaving the _other_ one mounted but dead. First-wins is the only order\nwhere release always precedes acquire: the loser never registers, and when\nthe winner unmounts (removing its own action, correctly) the claim passes to\nwhichever declaration is still mounted, registering in a later commit. To let\na screen override a shortcut, give it its own name, or move the declaration\nsomewhere both screens can reach.\n\nInside a `Modal`, the enclosing window is the modal's own, so actions and\ncontrollers declared there belong to it and go away with it — usually what a\ndialog wants. Under `chrome: \"content\"` and inside the navigators nothing\nchanges: the window is still the one `AppRegistry` built, the navigators own\nwidgets inside it rather than its action map, and a HeaderBar button in a\npage resolves `win.*` up through the widget hierarchy to that same window.\nOne consequence worth knowing: react-navigation keeps a popped screen\nmounted until its exit transition ends, so a screen's actions and\ncontrollers outlive the pop by the length of the animation.",
1441
+ },
1442
+ {
1443
+ doc: "docs/architecture/integration.md",
1444
+ heading: "`Controllers`: a GTK event controller on a React Native component",
1445
+ text: "The same idea one level down — `Controllers` attaches its children to the\nwidget of the _enclosing_ React Native component, `View`, `Pressable`,\n`ScrollView`, `Animated.View`, any of them:\n\n```tsx\n<Pressable onPress={open}>\n <Controllers>\n <GtkDragSource\n actions={Gdk.DragAction.MOVE}\n onPrepare={(x, y, self) =>\n Gdk.ContentProvider.newForValue(\n GObject.buildValue(GObject.TYPE_STRING, (v) => v.setString(id)),\n )\n }\n />\n </Controllers>\n <Text>{title}</Text>\n</Pressable>\n```\n\n**Why it exists.** A `Pressable`'s `ref` is deliberately a `ViewHandle`\n(`measure`/`measureInWindow`/`measureLayout`) and not a `Gtk.Widget` — React\nNative's contract says nothing about widgets, and reaching a real GObject\nthrough a ref would pin every internal of this platform as public API. GTK\ncarries behavior no style and no RN prop expresses, drag-and-drop above all,\nand `Controllers` is how a row written in ordinary React Native reaches it.\n\n**Why a component, not a `controllers` prop on `View`.** A prop would sit on\na component an app shares with iOS and Android, imported from the _portable_\nentry point — the file would compile everywhere, the prop would be ignored\noff Linux, and the feature would vanish with no diagnostic. Here the import\nitself is the signal: `react-native-gtkx/gtk` is a line an app already knows\nit's crossing, one it already knows how to gate behind `Platform.OS` or a\n`.linux.tsx` split — and its absence is visible in the tree rather than\nsilently inert.\n\nTwo properties follow from `Controllers` being a portal: **it composes with\ncontext** (the handler that reorders a list is written where that list's\nstate already lives), and **it's lifecycle-bound** (attached on mount,\nremoved on unmount, so a screen's controllers leave with the screen).\n\nOne caveat, stated plainly: controllers attach **one commit after mount**.\nReact attaches host refs bottom-up, so the enclosing view's widget doesn't\nexist yet when a child's own layout effects run. For an event controller\nthis is unobservable in practice — no pointer reaches a widget in its first\nframe — but it does mean a test aiming a synthetic pointer at a freshly\nmounted tree has to let one commit land first.\n\nInside a GTK widget's own slot there's no enclosing React Native component\nand nothing to attach to; pass `controllers={…}` to the widget itself there\ninstead — the prop `Controllers` substitutes for everywhere else.\n\n`react-native-gtkx/dnd` mirrors `react-native-reanimated-dnd`'s API\n(`Draggable`, `Droppable`, `DropProvider`, `Sortable`) on top of exactly\nthese two controllers, and both bundler presets alias that package name onto\nit — so does `react-native-gesture-handler`, onto a shim keeping\n`GestureHandlerRootView` working — meaning a ported app's drag-and-drop\nsource runs unchanged.",
1446
+ },
1447
+ {
1448
+ doc: "docs/architecture/integration.md",
1449
+ heading: "GSettings",
1450
+ text: '`useSetting` and `useBindSetting` come from `@gtkx/react`, re-exported from\n`react-native-gtkx/gtk` next to the `Gio` namespace they read and write\nthrough:\n\n```tsx\nconst [value, setValue] = useSetting(schema, "color-scheme")\nuseBindSetting({\n schema,\n key: "window-width",\n object: windowRef,\n property: "defaultWidth",\n})\n```\n\nTurning a `.gschema.xml` file into the `SettingsSchema` object these hooks\nexpect (`{ id, path, keys }`) is a build-time concern, not something this\nsubpath does. It resolves for free on the `gtkx dev`/`gtkx build` toolchain —\nthe `gtkx:settings` vite plugin ships inside `@gtkx/cli` itself — but it is\nnot wired into the Metro toolchain (`react-native run-linux`) at all; an app\non that path constructs the `SettingsSchema` object by hand\n(`{ id, path, keys: { "key-name": "s" } }`, matching the schema\'s own type\nstrings) or adds its own build step.',
1451
+ },
1452
+ {
1453
+ doc: "docs/architecture/integration.md",
1454
+ heading: "Related",
1455
+ text: "- [Overview](overview) — where `NavigationStack`, `Widget`, and the rest of\n `react-native-gtkx/common` come from, and the widget taxonomy `Controllers`\n and the action components sit alongside.\n- [Layout and styling](layout-and-styling) — `SlotContent`/`IntrinsicContent`,\n used throughout the navigation examples above.",
1456
+ },
1457
+ {
1458
+ doc: "docs/architecture/gestures.md",
1185
1459
  heading: "Three layers, in the order to reach for them",
1186
- text: "**1. `Pressable`** — taps, long presses, hover and keyboard activation. It is\nnot built on the responder system, it takes no negotiation, and it is what\nalmost every interaction actually needs.\n\n```tsx\n<Pressable\n onPress={open}\n onLongPress={showMenu}\n hitSlop={8}\n style={({ pressed, hovered, focused }) => [\n styles.row,\n hovered && styles.rowHovered,\n pressed && styles.rowPressed,\n focused && styles.rowFocused,\n ]}\n>\n <Text>Open</Text>\n</Pressable>\n```\n\n`hitSlop` widens the target without changing the layout; `pressRetentionOffset`\nsets how far the pointer may drift after pressing and still activate on\nrelease (RN's default rect, `{top: 20, left: 20, right: 20, bottom: 30}`, is\nalready generous). A release outside that rect is a cancel — dragging off a\ncontrol to change your mind works the way it does everywhere else.\n\n**2. The responder system and `PanResponder`** — drags, pans, swipes, and\nanything that needs to decide _which_ view owns an interaction.\n\n```tsx\nconst pan = useRef(new Animated.ValueXY()).current\nconst responder = useRef(\n PanResponder.create({\n // Claim on press, or wait for movement — the choice matters, see\n // \"Claiming on press versus on move\" below.\n onMoveShouldSetPanResponder: (_event, gesture) => Math.abs(gesture.dx) > 8,\n onPanResponderMove: (_event, gesture) => {\n pan.setValue({ x: gesture.dx, y: gesture.dy })\n },\n onPanResponderRelease: () => {\n // The continuing-drag idiom: fold where it ended into the offset so\n // the next gesture's dx starts from zero instead of snapping back.\n pan.extractOffset()\n pan.setValue({ x: 0, y: 0 })\n },\n }),\n).current\n\nreturn (\n <Animated.View\n {...responder.panHandlers}\n style={{ transform: pan.getTranslateTransform() }}\n />\n)\n```\n\nResponder and touch props go on `View` and `Animated.View`. Spreading\n`panHandlers` onto anything else compiles and does nothing, which is worth\nknowing because the idiomatic drag target is `Animated.View`.\n\n`PanResponder` here is react-native's own file, vendored unmodified, running\nagainst a reproduction of RN's `touchHistory` store — so `dx`/`dy`/`vx`/`vy`\nand the clustered-touch maths are upstream's, not a reimplementation.\n\n**3. GTK event controllers** — anything GTK does that React Native has no\nword for. Drag-and-drop with real drag icons and content negotiation, zoom\nand rotate gestures, keyboard shortcut controllers. This is a Linux-only\nescape hatch and the import says so:\n\n```tsx\nimport { Controllers, GtkDragSource } from \"react-native-gtkx/gtk\"\n\n;<Pressable onPress={open}>\n <Controllers>\n <GtkDragSource\n actions={Gdk.DragAction.MOVE}\n onPrepare={prepare}\n />\n </Controllers>\n <Text>{task.title}</Text>\n</Pressable>\n```\n\nSee [the platform layer](platform-layer.md). For drag-and-drop of any\nshape, including a reorderable list,\n[`react-native-gtkx/dnd`](api.md#drag-and-drop-react-native-gtkxdnd) already\nwraps this.",
1460
+ text: "**1. `Pressable`** — taps, long presses, hover and keyboard activation. It\nisn't built on the responder system, it takes no negotiation, and it's what\nalmost every interaction actually needs.\n\n```tsx\n<Pressable\n onPress={open}\n onLongPress={showMenu}\n hitSlop={8}\n style={({ pressed, hovered, focused }) => [\n styles.row,\n hovered && styles.rowHovered,\n pressed && styles.rowPressed,\n focused && styles.rowFocused,\n ]}\n>\n <Text>Open</Text>\n</Pressable>\n```\n\n`hitSlop` widens the target without changing layout; `pressRetentionOffset`\nsets how far the pointer may drift after pressing and still activate on\nrelease (RN's default rect, `{top: 20, left: 20, right: 20, bottom: 30}`, is\nalready generous). A release outside that rect cancels — dragging off a\ncontrol to change your mind works the way it does everywhere else.\n\n**2. The responder system and `PanResponder`** — drags, pans, swipes, and\nanything that needs to decide _which_ view owns an interaction.\n\n```tsx\nconst pan = useRef(new Animated.ValueXY()).current\nconst responder = useRef(\n PanResponder.create({\n // Claim on press, or wait for movement — the choice matters, see\n // \"Claiming on press versus on move\" below.\n onMoveShouldSetPanResponder: (_event, gesture) => Math.abs(gesture.dx) > 8,\n onPanResponderMove: (_event, gesture) => {\n pan.setValue({ x: gesture.dx, y: gesture.dy })\n },\n onPanResponderRelease: () => {\n // The continuing-drag idiom: fold where it ended into the offset so\n // the next gesture's dx starts from zero instead of snapping back.\n pan.extractOffset()\n pan.setValue({ x: 0, y: 0 })\n },\n }),\n).current\n\nreturn (\n <Animated.View\n {...responder.panHandlers}\n style={{ transform: pan.getTranslateTransform() }}\n />\n)\n```\n\nResponder and touch props go on `View` and `Animated.View`; spreading\n`panHandlers` onto anything else compiles and does nothing, which is worth\nknowing because the idiomatic drag target is `Animated.View`.\n\n`PanResponder` here is react-native's own file, vendored unmodified, running\nagainst a reproduction of RN's `touchHistory` store — so `dx`/`dy`/`vx`/`vy`\nand the clustered-touch maths are upstream's, not a reimplementation.\n\n**3. GTK event controllers** — anything GTK does that React Native has no\nword for: drag-and-drop with real drag icons and content negotiation, zoom\nand rotate gestures, keyboard shortcut controllers. This is a Linux-only\nescape hatch, and the import says so:\n\n```tsx\nimport { Controllers, GtkDragSource } from \"react-native-gtkx/gtk\"\n\nconst row = (\n <Pressable onPress={open}>\n <Controllers>\n <GtkDragSource\n actions={Gdk.DragAction.MOVE}\n onPrepare={prepare}\n />\n </Controllers>\n <Text>{task.title}</Text>\n </Pressable>\n)\n```\n\nSee [Window, navigation, and settings](integration) for `Controllers` in\nfull. For drag-and-drop of any shape, including a reorderable list,\n`react-native-gtkx/dnd` (see the [Reference](../reference)) already wraps\nthis.",
1187
1461
  },
1188
1462
  {
1189
- doc: "docs/gestures.md",
1463
+ doc: "docs/architecture/gestures.md",
1190
1464
  heading: "How the negotiation works",
1191
- text: "One interaction lock for the whole process, as in RN. On a press, the\n`*ShouldSetResponderCapture` handlers run from the root down and the\n`*ShouldSetResponder` handlers from the target up; the first to return `true`\nwins. While the pointer is down the lock can move **upwards** — an ancestor\nthat returns `true` from `onMoveShouldSetResponder` takes it, provided the\ncurrent holder does not refuse with `onResponderTerminationRequest`. A\ndescendant can never take it from an ancestor.\n\nThe negotiation path stops at the layout root. A React Native tree here can\nbe an island inside a native GTK widget tree, so native widgets above or\nbetween views simply take no part.",
1465
+ text: "One interaction lock for the whole process, as in RN. On a press, the\n`*ShouldSetResponderCapture` handlers run from the root down and the\n`*ShouldSetResponder` handlers from the target up; the first to return\n`true` wins. While the pointer is down, the lock can move **upwards** — an\nancestor that returns `true` from `onMoveShouldSetResponder` takes it,\nprovided the current holder doesn't refuse with\n`onResponderTerminationRequest`. A descendant can never take it from an\nancestor.\n\nThe negotiation path stops at the layout root. A React Native tree here can\nbe an island inside a native GTK widget tree, so native widgets above or\nbetween views simply take no part.",
1192
1466
  },
1193
1467
  {
1194
- doc: "docs/gestures.md",
1468
+ doc: "docs/architecture/gestures.md",
1195
1469
  heading: "Claiming on press versus on move",
1196
- text: "Claiming on press (`onStartShouldSetPanResponder`) tells GTK the interaction\nis yours before anything else can compete for it. Claiming on movement is the\nmore common shape and is slightly weaker: inside a scrolling list on a\ntouchscreen, the scroller can take the first few pixels before your threshold\nis reached. Claim on press when the view is unambiguously a drag handle.",
1470
+ text: "Claiming on press (`onStartShouldSetPanResponder`) tells GTK the interaction\nis yours before anything else can compete for it. Claiming on movement is\nthe more common shape, and slightly weaker: inside a scrolling list on a\ntouchscreen, the scroller can take the first few pixels before your\nthreshold is reached. Claim on press when the view is unambiguously a drag\nhandle.",
1197
1471
  },
1198
1472
  {
1199
- doc: "docs/gestures.md",
1473
+ doc: "docs/architecture/gestures.md",
1200
1474
  heading: "Where this differs from React Native",
1201
- text: "The full reasoning for each is in\n[docs/research/gestures.md](research/gestures.md); the short version:\n\n**Terminations mostly cannot be refused.** In React Native and\nreact-native-web, `onResponderTerminationRequest` is consulted for a context\nmenu, an ancestor scroll and a selection change. Here it is consulted in\nexactly two situations — a transfer to an ancestor, and an enclosing\n`ScrollView` scrolling. Everything else (a second mouse button, a native\nwidget or a `GtkDragSource` taking the sequence, text selection) reaches JS\nonly _after_ GTK has already taken the interaction away, and GTK's claim\ncannot be given back. Those arrive as `onResponderTerminate` with no question\nasked. Window blur also terminates unconditionally, which is RN's behaviour\ntoo.\n\n**One pointer.** A mouse is one fabricated touch; `touches` never has more\nthan one entry, and multi-finger `gestureState` is single-touch. Pinch and\nrotate are not available through the portable API — use `GtkGestureZoom` /\n`GtkGestureRotate` through `Controllers` if you need them on Linux.\n\n**ScrollView arbitration is touch-only.** All four gestures\n`GtkScrolledWindow` runs internally are touch-only, so under a mouse a child\npan never competes with scrolling at all. On touch, a view that takes the\nresponder suspends the enclosing scroller for the rest of the interaction\n(RN's `setIsJSResponder`). Scrolling with a **wheel** during a gesture is not\nsuppressed — it terminates the responder instead, which is react-native-web's\nrule for an ancestor scroll.\n\n**Hover fires from touch.** react-native-web filters hover events that come\nfrom a finger; GTK crossing events carry no device to filter on, GTK sends a\nmatching leave when a touch ends so no phantom hover sticks, and GTK's own\n`:hover` behaves the same way. Filtering here would make `Pressable` the odd\nwidget out in its own window.\n\n**`hitSlop` stops at a clip.** GTK stops picking at a clipping ancestor, so\nslop cannot escape a `ScrollView` viewport, or any view whose style says\n`overflow: \"hidden\"` — the same limit RN documents on Android.\n\n**No `Animated.event`.** Write the value directly\n(`pan.setValue({x: gesture.dx, y: gesture.dy})`), which is what it would do.",
1475
+ text: "**Terminations mostly cannot be refused.** In React Native and\nreact-native-web, `onResponderTerminationRequest` is consulted for a context\nmenu, an ancestor scroll, and a selection change. Here it's consulted in\nexactly two situations — a transfer to an ancestor, and an enclosing\n`ScrollView` scrolling. Everything else (a second mouse button, a native\nwidget or a `GtkDragSource` taking the sequence, text selection) reaches JS\nonly _after_ GTK has already taken the interaction away, and GTK's claim\ncan't be given back those arrive as `onResponderTerminate` with no\nquestion asked. Window blur also terminates unconditionally, which is RN's\nbehavior too.\n\n**One pointer.** A mouse is one fabricated touch; `touches` never has more\nthan one entry, and multi-finger `gestureState` is single-touch. Pinch and\nrotate aren't available through the portable API — use `GtkGestureZoom` /\n`GtkGestureRotate` through `Controllers` for them on Linux.\n\n**ScrollView arbitration is touch-only.** All four gestures\n`GtkScrolledWindow` runs internally are touch-only, so under a mouse a child\npan never competes with scrolling at all. On touch, a view that takes the\nresponder suspends the enclosing scroller for the rest of the interaction\n(RN's `setIsJSResponder`). Scrolling with a **wheel** during a gesture isn't\nsuppressed — it terminates the responder instead, react-native-web's rule\nfor an ancestor scroll.\n\n**Hover fires from touch.** react-native-web filters hover events coming\nfrom a finger; GTK crossing events carry no device to filter on, GTK sends a\nmatching leave when a touch ends so no phantom hover sticks, and GTK's own\n`:hover` behaves the same way. Filtering here would make `Pressable` the odd\nwidget out in its own window.\n\n**`hitSlop` stops at a clip.** GTK stops picking at a clipping ancestor, so\nslop can't escape a `ScrollView` viewport, or any view whose style says\n`overflow: \"hidden\"` — the same limit RN documents on Android.\n\n**No `Animated.event`.** Write the value directly\n(`pan.setValue({x: gesture.dx, y: gesture.dy})`), which is what it would do\nanyway.",
1202
1476
  },
1203
1477
  {
1204
- doc: "docs/gestures.md",
1478
+ doc: "docs/architecture/gestures.md",
1205
1479
  heading: "Porting an app",
1206
- text: "Both `react-native-reanimated` and `react-native-gesture-handler` are aliased\nonto reimplementations by both presets, so their imports resolve and their\n`Pan` code runs unedited. What is implemented of RNGH is\n`GestureHandlerRootView`, `GestureDetector`, `State`, `Pan`, `Tap`,\n`LongPress` and `Native` in both spellings (`Gesture.Pan()` and\n`usePanGesture()`, and so on), the `Race`/`Simultaneous`/`Exclusive` composers\nand the cross-gesture relations (`simultaneousWithExternalGesture`,\n`requireExternalGestureToFail`, `blocksExternalGesture` — arbitrated in a\nsecond, JS-only registry over the responder lock, because the lock has one\nholder by design and simultaneity is a set), and the components it re-exports\nfrom `react-native` — `ScrollView`, `FlatList`, `TextInput`, `Switch`,\n`Pressable` and the three `Touchable`s. See\n[the API reference](api.md#react-native-gesture-handler-react-native-gtkxgesture-handler)\nfor the tables, and the gallery's \"Gesture detector\" and \"Gesture\nrelations\" sections for the shapes running.\n\nWhat is not implemented throws where it is used, naming itself, rather than\nsilently doing nothing:\n\n- **`Pinch` and `Rotation`** — GTK feeds touchpad gestures properly, and\n nothing in this project's test rig can produce one, so they wait for a\n machine that can;\n- **`Fling`, `Hover`, `Manual`, `ForceTouch`**, the legacy `*GestureHandler`\n components and the button family (`RectButton` and friends — RNGH's own\n native button views, not RN components with a handler attached).\n\n**`react-native-draggable-flatlist` 4.0.3 and `@gorhom/bottom-sheet` 5.2.14\nboth run**, and neither was stopped by this surface in the end: what they\nneeded was four `react-native` core exports (`findNodeHandle`, `LogBox`,\n`Keyboard`, `VirtualizedList`) and Reanimated's `useAnimatedScrollHandler`,\nall of which ship now. That was established by BUILDING them and then driving\nthem with a real pointer rather than by reading their imports — twice a list\nof blockers derived from sources turned out to be wrong. The probe app is\n`spike/core-exports`; the API reference has the per-library detail and what\nthe probe does not prove.\n\nWhat to do instead, where something is still missing:\n\n- a **drag** — `PanResponder` plus `Animated.ValueXY`, as above. Portable,\n and it is what most RNGH usage in the wild amounts to;\n- **drag and drop between zones, or a sortable list** —\n [`react-native-gtkx/dnd`](api.md#drag-and-drop-react-native-gtkxdnd)\n mirrors `react-native-reanimated-dnd`'s API on GTK's own drag-and-drop,\n and both presets alias that package name onto it;\n- **swipeable rows** — by hand today: `PanResponder` for the gesture, plus\n either `Animated` or\n [`react-native-gtkx/reanimated`](api.md#react-native-reanimated-react-native-gtkxreanimated)\n for the motion. A **bottom sheet** no longer needs the hand-rolled version:\n `@gorhom/bottom-sheet` runs (see above), and `AdwBottomSheet` is the native\n one a Linux-first app would reach for instead.\n\n`examples/gallery`'s Gestures section is a working reference written entirely\nin portable `react-native`, with no platform-layer import in it at all.",
1480
+ text: "Both `react-native-reanimated` and `react-native-gesture-handler` are\naliased onto reimplementations by both bundler presets, so their imports\nresolve and their `Pan` code runs unedited. What's implemented of RNGH is\n`GestureHandlerRootView`, `GestureDetector`, `State`, `Pan`, `Tap`,\n`LongPress` and `Native` in both spellings (`Gesture.Pan()` and\n`usePanGesture()`, and so on), the `Race`/`Simultaneous`/`Exclusive`\ncomposers and the cross-gesture relations (`simultaneousWithExternalGesture`,\n`requireExternalGestureToFail`, `blocksExternalGesture` — arbitrated in a\nsecond, JS-only registry over the responder lock, because the lock has one\nholder by design and simultaneity is a set), plus the components it\nre-exports from `react-native` — `ScrollView`, `FlatList`, `TextInput`,\n`Switch`, `Pressable` and the three `Touchable`s.\n\nWhat isn't implemented throws where it's used, naming itself, rather than\nsilently doing nothing:\n\n- **`Pinch` and `Rotation`** — GTK feeds touchpad gestures properly, and\n nothing in this project's test rig can produce one, so they wait for a\n machine that can;\n- **`Fling`, `Hover`, `Manual`, `ForceTouch`**, the legacy `*GestureHandler`\n components, and the button family (`RectButton` and friends — RNGH's own\n native button views, not RN components with a handler attached).\n\n`react-native-draggable-flatlist` 4.0.3 and `@gorhom/bottom-sheet` 5.2.14\nboth run, and neither was stopped by this surface in the end: what they\nneeded was four `react-native` core exports (`findNodeHandle`, `LogBox`,\n`Keyboard`, `VirtualizedList`) and Reanimated's `useAnimatedScrollHandler`,\nall of which ship. This is verified by building both and driving them with a\nreal pointer, not by reading their imports — the probe app is\n`spike/core-exports`; the Reference has the per-library detail.\n\nWhat to do instead, where something is still missing:\n\n- a **drag** — `PanResponder` plus `Animated.ValueXY`, as above. Portable,\n and what most RNGH usage in the wild amounts to;\n- **drag and drop between zones, or a sortable list** —\n `react-native-gtkx/dnd` mirrors `react-native-reanimated-dnd`'s API on\n GTK's own drag-and-drop, and both presets alias that package name onto it;\n- **swipeable rows** — by hand today: `PanResponder` for the gesture, plus\n either `Animated` or `react-native-gtkx/reanimated` for the motion. A\n **bottom sheet** no longer needs the hand-rolled version:\n `@gorhom/bottom-sheet` runs (see above), and `AdwBottomSheet` is the\n native one a Linux-first app reaches for instead.\n\n`examples/gallery`'s Gestures section is a working reference written\nentirely in portable `react-native`, with no platform-layer import in it at\nall.",
1207
1481
  },
1208
1482
  {
1209
- doc: "docs/gtkx-rc4-notes.md",
1210
- heading: "RC4-WORKAROUND(use-signal-stale-handler)",
1211
- text: "`useSignal` still routes the handler through React's `useEffectEvent`; `react-reconciler@0.33.0` only refreshes it in `commitBeforeMutationEffects` for `case 0` (FunctionComponent) — `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fall through unrefreshed, so any `useEffectEvent` in a `memo`/`forwardRef` component is pinned to its mount closure forever (our `ScrollView` is a `forwardRef` with the `useSignal` calls inside it) — a fetch-fed FlatList empties itself on the first scroll. **rc.4 did not bump React and did not change the path** see below — our workaround: `gtkx/bridge/use-signal.ts` re-pins the latest handler (insertion effect) and hands gtkx a stable wrapper; the bridge exports that hook, not gtkx's — removed when: A stable React 19.3 (React fixed the refresh on the 19.3 line)",
1483
+ doc: "docs/architecture/gestures.md",
1484
+ heading: "Related",
1485
+ text: "- [Overview](overview) — the widget and subpath structure `Controllers`\n and `GtkDragSource` sit inside.\n- [Window, navigation, and settings](integration) — `Controllers` in full.",
1212
1486
  },
1213
1487
  {
1214
- doc: "docs/gtkx-rc4-notes.md",
1215
- heading: "RC4-WORKAROUND(runtime-dedupe)",
1216
- text: "Two bundled copies of the gtkx runtime still double-init GLib and abort; nothing guards against it. Reproduced on rc.4: `g_log_set_writer_func() called multiple times`, SIGABRT our workaround: `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits removed when: Idempotent runtime init upstream, or an error that names the duplicate",
1488
+ doc: "docs/architecture/glossary.md",
1489
+ heading: "Platform",
1490
+ text: "react-native-gtkx itself: the `react-native` API, rendered as native\nGTK4/libadwaita widgets on Linux through gtkx (a React reconciler with an\nin-process FFI into libgtk) and Yoga (layout). Linux is a React Native\n[out-of-tree platform](https://reactnative.dev/docs/out-of-tree-platforms)\nhere, the same model react-native-windows and react-native-macos use see\n[Overview](overview).",
1217
1491
  },
1218
1492
  {
1219
- doc: "docs/gtkx-rc4-notes.md",
1220
- heading: "RC4-WORKAROUND(prop-portal)",
1221
- text: '`createPortal(children, container, key?)` is byte-for-byte the rc.3 signature and can still only target a container\'s DEFAULT slot ("children"). Every other slot an object exposes declaratively a window\'s `Gio.ActionMap` (`actions`), a widget\'s `controllers`, an `AdwApplicationWindow`\'s `breakpoints` — is reached only by passing an element-valued PROP, which the reconciler routes through an internal `"gtkx:prop"` element. rc.4 exports that element from neither `@gtkx/react` nor `/internal`, and its `exports` map now refuses the deep path outright — our workaround: `gtkx/bridge/slot-portal.ts` restates the `"gtkx:prop"` element name and wraps it in `createSlotPortal(children, target, slot)` — the one line that would move if gtkx renames it. `WindowActions`/`ApplicationActions`/`WindowControllers` are built on it removed when: gtkx exports a slot-aware portal (or the prop element) from a public entry point',
1493
+ doc: "docs/architecture/glossary.md",
1494
+ heading: "Host",
1495
+ text: "The Node process a bundle actually runs inside the JS runtime plus the\nnative GTK addon (`@gtkx/*`) loaded into itas distinct from the bundle\nitself, which a bundler produces but does not run. On the Metro path this is\n`run-linux`'s own host (`dist/runner/host.js`); on the vite path, the\n`gtkx dev`/`gtkx build` process fills the same role. See\n[Packaging](../guide/packaging.md) and [Running without\nlibadwaita](../guide/plain-gtk.md#how-the-profile-is-detected).",
1222
1496
  },
1223
1497
  {
1224
- doc: "docs/gtkx-rc4-notes.md",
1225
- heading: "RC4-WORKAROUND(renderhook-no-window)",
1226
- text: "`renderHook` still mounts into a bare `Gtk.Box` — rc.4's `render-hook.js` is byte-identical to rc.3's, which was byte-identical to rc.2's so window-dependent APIs have no toplevel to read — our workaround: Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) — removed when: `renderHook` mounts into the same harness window `render` uses",
1498
+ doc: "docs/architecture/glossary.md",
1499
+ heading: "Bridge",
1500
+ text: "`src/gtkx/bridge/` the one place in this package's source that imports\n`@gtkx/*` (eslint-enforced). gtkx is a young dependency, so its API drift\nis absorbed here instead of at every call site; each workaround this forces\ncarries a `1.2-WORKAROUND` tag, cataloged in `docs/gtkx-1.2-notes.md`. See\n[Overview](overview).",
1227
1501
  },
1228
1502
  {
1229
- doc: "docs/api.md",
1230
- heading: "View",
1231
- text: "View GTK implementation: GtkBox (RnGtkxViewBox). Supported: `style`, `onLayout`, `testID`, children, `pointerEvents` (auto/none/box-none/box-only mapped onto GTK picking: can-target + a contains() vfunc override; also honored from `style.pointerEvents`, the prop wins), `focusable` + `onFocus`/`onBlur` (RN has `focusable` on View for Android/Windows; react-native-web and react-native-windows both have the callbacks — off by default, as in RN), ref: `measure`/`measureInWindow`/`measureLayout` (`ViewHandle`, RN's argument order; window coordinates come from `gtk_widget_compute_point`, so they are correct inside a scrolled viewport), the responder and touch props ([guide](gestures.md)) (`onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` + `Capture`) — spread `PanResponder`'s `panHandlers` here. Differences from RN: Responder negotiation is RN's in full — capture-then-bubble, transfer to an ancestor mid-gesture through `onResponderTerminationRequest`/`onResponderReject`, and `onResponderTerminate`. The lock is one per process as in RN; the negotiation PATH stops at the layout root, so native GTK widgets between or above views take no part. Single-pointer only: a mouse is one fabricated touch, `touches` never exceeds one. Terminations differ from RN's, because GTK decides most of them before JS is told: a context menu (a second mouse button), a native widget or a `Controllers` `GtkDragSource` taking the sequence, and text selection all arrive as a cancelled gesture and terminate **without** consulting `onResponderTerminationRequest` GTK's `CLAIMED` is irrevocable, so there is nothing an answer could change. Window blur terminates unconditionally, as it does in react-native-web. An enclosing `ScrollView` scrolling under the gesture is the one termination the holder may refuse. `overflow: \"hidden\"` (and `\"scroll\"`, which clips identically — a `View` is not made scrollable by a style on this platform any more than it is in RN) clips both the paint AND the picking of the children, including transformed ones and children driven out by an animated absolute `top`/`left`: GTK pushes a clip node over the container's CSS padding box before it snapshots them, and `gtk_widget_pick()` refuses the same box, so what you cannot see you cannot click. `borderRadius` shapes that clip — a rounded container clips its children to the rounded corners. A container never clips its OWN background, border, shadow or outline, only its children's",
1503
+ doc: "docs/architecture/glossary.md",
1504
+ heading: "Profile",
1505
+ text: "Whether an app links libadwaita: **Adw** (the default Adwaita widgets,\ntheming, native chrome) or **plain GTK** (no libadwaita at all). The choice\nis per-app, made once in `gtkx.config.ts`'s `libraries`, not a runtime flag —\nboth are real, supported profiles, not a tier system. Every Reference entry\ncarries its own `**Profile:**` fact (or, for a whole subpath page, a\n`profile:` frontmatter key), checked against the code by `docs:check`. See\n[Running without libadwaita](../guide/plain-gtk.md).",
1232
1506
  },
1233
1507
  {
1234
- doc: "docs/api.md",
1235
- heading: "Text",
1236
- text: "Text GTK implementation: GtkLabel (Pango). Supported: wrap, `numberOfLines` (ellipsize END), `textAlign`, font styles, `onLayout`, `testID`, ref: `measure`/`measureInWindow`/`measureLayout` (`TextHandle` RN gives every host component the geometry methods, so a label no longer has to be wrapped in a `View` to be measurable). Differences from RN: nested `Text` elements are concatenated without per-span styles; text is always ellipsizable (shrinkable in narrow windows)",
1508
+ doc: "docs/architecture/glossary.md",
1509
+ heading: "Toolchain",
1510
+ text: "One of the two ways a project builds and runs: the **Metro path**\n(`npx react-native run-linux`/`build-linux`, for a project that already has\n`ios`/`android`) or the **vite path** (`gtkx dev`/`gtkx build`, what the\nLinux-first template uses). Both consume the same published package and\nrender through the same bridge the choice is about where a project comes\nfrom, not a difference in what it can do. See\n[Toolchains](../guide/toolchains.md).",
1237
1511
  },
1238
1512
  {
1239
- doc: "docs/api.md",
1240
- heading: "Image",
1241
- text: 'Image GTK implementation: GtkPicture. Supported: `source={{uri}}`/string local paths, file:// and **http(s)** (Node fetch disk cache keyed by URL, in-flight de-duplication), `resizeMode` cover/contain/stretch/center, `onLoad`/`onError`; **`.svg` files load like any other image** — `Gdk.Texture.newFromFilename` rasterizes them via librsvg, no extra code needed (for building vector graphics from state instead of a file, see the "Svg" section below a separate import, not part of this table); ref: `measure`/`measureInWindow`/`measureLayout` (`ImageHandle`). Differences from RN: no synchronous size from remote images (style sets the size, as in RN); cache is not size-limited yet',
1513
+ doc: "docs/architecture/glossary.md",
1514
+ heading: "Surface",
1515
+ text: 'The exported set of components/APIs at a given import boundary the\nportable surface (everything reachable from `"react-native"`), the widget\nsurface (every `Gtk.Widget`/`Adw.Widget` subclass gtkx binds, wrapped, raw or\nauxiliary), and so on for `react-native-gtkx/common`, `/gtk`, `/adw` and the\ncompat subpaths. "Surface" names what a subpath actually exports, checked\nagainst the Reference by `docs:check`. See\n[Overview](overview#the-widget-surface-wrapped-raw-and-auxiliary) and\n[Reference](../reference).',
1242
1516
  },
1243
1517
  {
1244
- doc: "docs/api.md",
1245
- heading: "TextInput",
1246
- text: "TextInputGTK implementation: GtkEntry / GtkTextView. Supported: controlled/uncontrolled (`value`/`defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus`/`onBlur`, `placeholder` (own dim overlay in multiline GtkTextView has none), `secureTextEntry`, `editable`, `keyboardType`, `multiline`, `clearButtonMode` (GtkEntry's built-in clear icon; RN ships this on iOS only), the visual half of `style` (background, border, radius — it used to be computed and dropped, so a styled TextInput silently kept the theme's own frame) (real GtkTextView: word wrap, internal scroll, Enter inserts a newline and never fires onSubmitEditing RN semantics). Differences from RN: multiline needs a height in the style (as RN recommends)",
1518
+ doc: "docs/architecture/glossary.md",
1519
+ heading: "Layout root",
1520
+ text: "Where a `LayoutEngine` one Yoga tree is created. There are three: the\nwindow root (`AppRegistry.runApplication`), a nested root (`SlotContent`,\nfills its slot) and an intrinsic root (`IntrinsicContent`, sized by its own\ncontent); a test renders into a fourth, `<Root>`. The gesture responder\nsystem's negotiation and a `ScrollView`'s clip both stop at a layout root's\nboundary, since a React Native tree here can be an island inside a native GTK\nwidget tree. See [Layout and styling](layout-and-styling#three-flavors-of-layout-root).",
1247
1521
  },
1248
1522
  {
1249
- doc: "docs/api.md",
1250
- heading: "Pressable",
1251
- text: "Pressable GTK implementation: GtkBox (RnGtkxViewBox) + GestureClick/Motion. Supported: `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn/Out`, `onFocus`/`onBlur`, `focusable`, `disabled`, function-form `style`/`children` receiving `{pressed, hovered, focused}` (react-native-web's own state shape); **keyboard-operable**: `focusable` defaults to true when `onPress` is set (react-native-web's rule), which puts the view in GTK's focus chain so Tab and the arrow keys reach it, and Enter/Space fire `onPress` as they do on web and Android; the `PressEvent` payload is RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, monotonic `timestamp`, single-element `touches`/`changedTouches` — a desktop pointer is one fabricated touch). `hitSlop` and `pressRetentionOffset`, each a number or per-edge; the press rect defaults to RN's own `{top: 20, left: 20, right: 20, bottom: 30}` around the hit rect, and a release outside it is a cancel rather than a press. Differences from RN: `hitSlop` cannot escape an ancestor that clips a `ScrollView` viewport, or any view with `overflow: \"hidden\"` — because GTK stops picking at the clip, which is the limit RN documents on Android for the same reason. Hover fires from touch as well as from a mouse: react-native-web filters that out, and here a GTK crossing event carries no device to filter on; GTK also sends a matching leave when a touch sequence ends, so the stuck phantom hover the filter exists for does not arise, and GTK's own `:hover` behaves the same way (docs/research/gestures.md)",
1523
+ doc: "docs/architecture/glossary.md",
1524
+ heading: "Portable",
1525
+ text: 'Code that imports only from `"react-native"` (or an upstream package\nresolved onto its compat subpath by the aliasing both presets apply) and\ntherefore runs unchanged on iOS, Android and web too. Importing\n`react-native-gtkx/gtk`, `/adw`, `/common` or a raw `@gtkx/*` module is\nLinux-only by definition — the import itself is what marks a line as no\nlonger portable. See [Installation](../guide/installation.md) and\n[Overview](overview#three-subpaths-beneath-the-portable-surface).',
1252
1526
  },
1253
1527
  {
1254
- doc: "docs/api.md",
1255
- heading: "TouchableOpacity",
1256
- text: "TouchableOpacityGTK implementation: on top of Pressable. Supported: `activeOpacity`. Differences from RN: —",
1528
+ doc: "docs/guide/installation.md",
1529
+ heading: "Prerequisites",
1530
+ text: "- Linux (x64/arm64, glibc), GTK4 ≥ 4.20, libadwaita ≥ 1.8 Ubuntu 26.04+\n or Fedora 43+ satisfy both out of the box;\n- Node.js ≥ 24;\n- development headers for codegen: `sudo apt install libgtk-4-dev\nlibadwaita-1-dev` (Ubuntu; the equivalent `-devel` packages on other\n distributions).\n\nRunning without libadwaita is also supported see [Running without\nlibadwaita](plain-gtk.md) for what that changes.",
1257
1531
  },
1258
1532
  {
1259
- doc: "docs/api.md",
1260
- heading: "TouchableHighlight",
1261
- text: "TouchableHighlight GTK implementation: on top of Pressable. Supported: `underlayColor` (RN default `black`), `activeOpacity`, `onShowUnderlay`/`onHideUnderlay`. Differences from RN: RN renders a separate underlay VIEW behind the child and dims the child onto it; here the highlight is the view's own `backgroundColor` while pressed, because an extra box would change flex layout and what `measureLayout` is relative to the same reason `GestureDetector` and `createAnimatedComponent` add none. Give the child a translucent background for RN's exact blend.",
1533
+ doc: "docs/guide/installation.md",
1534
+ heading: "New project from the template",
1535
+ text: '```bash\nnpx degit itsmepetrov/react-native-gtkx/template my-app\ncd my-app\nnpm install\nnpm run dev # a window opens, Fast Refresh applies edits live\n```\n\nProduction build:\n\n```bash\nnpm run build # single bundle: dist/bundle.mjs\nnpm start # node dist/bundle.mjs\n```\n\nMeasured in a clean Ubuntu 26.04 container, system dependencies\npreinstalled: 63 seconds from `npm install` to a window on screen.\n\nThe template\'s `vite.config.ts` wires the `react-native-gtkx/vite` preset\n(the `react-native` `react-native-gtkx` alias, Metro-style platform\nextensions); `gtkx dev`/`gtkx build` pick it up automatically. Its\n`tsconfig.json` maps the `"react-native"` specifier through `paths` so\neditor types resolve too. The default entry is `src/index.tsx`, and\n`Comp.tsx` next to `Comp.linux.tsx` builds the Linux variant for an\nextensionless `import { Comp } from "./Comp"` `Platform.select({ linux:\n…, native: …, default: … })` works exactly as it does in React Native and\nis tree-shaken out of the production build.',
1262
1536
  },
1263
1537
  {
1264
- doc: "docs/api.md",
1265
- heading: "TouchableWithoutFeedback",
1266
- text: "TouchableWithoutFeedbackGTK implementation: on top of Pressable. Supported: the `Pressable` press/hover/focus props, with no visual reaction. Differences from RN: RN clones its single child instead of rendering a box, which its own docs call a mistake kept for compatibility; this renders the `Pressable` box. Prefer `Pressable`, as RN's docs say.",
1538
+ doc: "docs/guide/installation.md",
1539
+ heading: "Add Linux to an existing React Native app",
1540
+ text: 'Linux is an [out-of-tree\nplatform](https://reactnative.dev/docs/out-of-tree-platforms) the same\nmodel react-native-windows and react-native-macos use. An app that\nalready has `ios/` and `android/` keeps them, keeps its Metro/Babel\ntoolchain, and gains one more target. Four steps:\n\n1. **Install the platform package:**\n\n ```bash\n npm install react-native-gtkx\n ```\n\n Its own `react-native.config.js` declares the `linux` platform and the\n `run-linux` command — nothing to declare app-side.\n\n2. **Wrap the Metro config** (`metro.config.js`):\n\n ```js\n const { getDefaultConfig } = require("@react-native/metro-config")\n const { withLinuxPlatform } = require("react-native-gtkx/metro")\n\n module.exports = withLinuxPlatform(getDefaultConfig(__dirname))\n ```\n\n This adds the platform (`.linux.tsx` extensions, `Platform.OS ===\n"linux"`), redirects `react-native` imports to the platform package, and\n keeps host-side modules (the GTK bindings, `react`, `yoga-layout`) out\n of the bundle — Metro cannot bundle native addons, and the reconciler\n needs to share one `react` instance with the app. Babel stays\n completely stock.\n\n3. **Add `gtkx.config.ts`** with the GTK application id:\n\n ```ts\n import { defineConfig } from "@gtkx/config"\n\n export default defineConfig({\n libraries: ["Gtk-4.0", "Adw-1"],\n applicationId: "com.example.myapp",\n })\n ```\n\n4. **Start the app from the entry** — on desktop the entry launches the\n app itself, the same pattern react-native-web uses for\n `index.web.js`:\n\n ```js\n // index.js, after AppRegistry.registerComponent(...)\n if (Platform.OS === "linux") {\n AppRegistry.runApplication(appName, {\n title: "My App",\n width: 800,\n height: 600,\n })\n }\n ```\n\nThat\'s the whole integration — see [Your first app](first-app.md) for\nrunning it. `examples/rn-app` in the repository is a complete cli-init\napp with all three platforms wired this way.\n\nA stock `@react-native-community/cli init` project\'s `App.tsx` renders\n`<NewAppScreen>` from `@react-native/new-app-screen`, which reaches into\n`react-native/Libraries/Core/Devtools/openURLInBrowser` — a deep import\noutside the supported `react-native` surface (see [Typed\ncode](#typed-code) below). Metro cannot resolve it for `--platform\nlinux`, so a fresh cli-init project fails to bundle until `App.tsx` is\nreplaced with your own component — [Your first app](first-app.md#hello-gnome)\nhas a minimal one to start from.',
1267
1541
  },
1268
1542
  {
1269
- doc: "docs/api.md",
1270
- heading: "ScrollView",
1271
- text: "ScrollView GTK implementation: GtkScrolledWindow. Supported: vertical/`horizontal`, `contentContainerStyle` (RN's default: the content container is a plain `View`, so `alignItems` is `stretch` children fill the cross axis unless they say otherwise), `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), `onScrollBeginDrag`/`onScrollEndDrag`/`onMomentumScrollBegin`/`onMomentumScrollEnd` (the four phases, each carrying the same `ScrollEvent` — see the Differences column), `onContentSizeChange`, `stickyHeaderIndices` (RN model: the REAL child is translated and painted on top — no duplicate), ref: `scrollTo`/`scrollToEnd` **plus the geometry methods** `measure`/`measureInWindow`/`measureLayout` (`ScrollViewHandle`). A view inside it that takes the responder suspends the scroller's own gestures for the rest of the interaction RN's `setIsJSResponder`, so a child pan is reachable inside a scrolling list. Differences from RN: `animated` in scrollTo is ignored. **The scroll phases are input-device-aware**: GTK gives a mouse wheel isolated detents, so this platform groups a burst into one desktop begin/end session (120 ms idle boundary) and reports no momentum; a touchpad glide reports all four from its native GTK sequence, and the content really does keep moving after the fingers leave. RN has no wheel, so the wheel session is an explicit desktop extension rather than a parity claim. `onScrollBeginDrag`/`onScrollEndDrag` map onto the user-driven scroll SESSION (GTK's `::scroll-begin`/`::scroll-end` for touchpad, the grouped burst for wheel) rather than onto a finger on the content, which is the one approximation: a touchpad never touches the content, so \"the user started driving this scroller\" is the closest true statement. The momentum pair is read off the adjustment actually moving after the sequence ended, not off `::decelerate` (which fires at every lift, velocity or none), so a glide that ends dead reports the drag pair and no momentum pair as RN does. **None of it is installed until a handler is attached**: no controller, no signal, no timer, and 6.93 µs per scroll event with all four attached against 7.17 µs with none, which is inside the noise; the GTK-controller residual is 0.31 µs per event while any phase handler is attached. A begin/end consumer also makes each wheel detent enter the 0.235 µs session state machine; momentum-only and untracked scrollers do not. Traces and numbers: [research/scroll-phases.md](research/scroll-phases.md). Scroll arbitration is **touch-only and unverified end to end**: all four gestures `GtkScrolledWindow` installs are touch-only, so under a mouse a child pan never competes with scrolling at all, and no touch can be injected on the test rig (wlroots has no virtual-touch protocol) every link of the mechanism is tested, the finger is not. Two known edges on touch: a view that claims on a MOVE rather than on press can lose the first ~8 px to the scroller, which `CLAIMED` makes irrevocable (iOS has the same artefact); and the mouse wheel is deliberately left alone, so scrolling with a wheel during a gesture terminates the responder rather than being suppressed. **The scroller carries RN's own base style, `flexGrow: 1, flexShrink: 1`** — `styles.baseVertical`/`baseHorizontal`, composed UNDER the app's `style` exactly as RN's `StyleSheet.compose(baseStyle, this.props.style)` composes it, on the same node the `style` prop lands on. `FlatList`, `SectionList` and `VirtualizedList` inherit it. It is what makes a scrollable with NO style of its own a viewport rather than a box grown to its content: libraries hand their list down unstyled and let the host size it (`@gorhom/bottom-sheet` is the one that found this — its scroll lock had nothing to lock, because a list that never became a viewport never emitted a scroll event). **The surprise is the other consequence**: an explicit main-axis `height` on the scroller is only its flex BASIS, so in a taller flex parent grow expands past it `<ScrollView style={{ height: 200 }}>` inside a 400px column lays out at 400, not 200. That is parity, not a deviation, and it was checked rather than assumed: RN 0.86.2 composes the same two properties under the same `style` prop on the same Yoga node, and in Yoga a definite main-axis length becomes the flex BASIS (`computeFlexBasisForChild`) before `resolveFlexibleLength` hands out the free space — neither function consults any config. The one Yoga setting the two platforms differ on is errata (RN runs the tree at `YGErrataAll`, this platform at Yoga's default `None`) and it is inert here: the only flex-relevant bit, `StretchFlexBasis`, is reached only when the CONTAINER's main size is indefinite, and a bounded parent's is not. `useWebDefaults` is false on both, which is exactly why the `flexShrink: 1` half is needed at all. To bound the viewport, bound the PARENT (`<View style={{ height: 200 }}><FlatList /></View>` — what an RN app writes) or cancel the base with `flexGrow: 0`",
1543
+ doc: "docs/guide/installation.md",
1544
+ heading: "Typed code",
1545
+ text: 'Add an `env.d.ts` with `import "react-native-gtkx/types"` — it augments\nthe stock `react-native` types so `Platform.select({ linux: ... })`\ntypechecks, and `Pressable`\'s state callback accepts `hovered` (declared\noptional, since a component shared with ios/android gets `undefined`\ntherewrite `hovered && styles.hovered`). One thing augmentation cannot\nteach is `Platform.OS === "linux"` as a type guard (property types don\'t\nmerge across an augmentation) — use `Platform.select` where the branch\nneeds to typecheck. Deep imports (`react-native/Libraries/...`) are not\nsupportedonly the public `react-native` surface is.\n\nBoth toolchains alias the bare `react-native-svg` import to\n`react-native-gtkx/svg` at build timesee [Toolchains: bundler\naliases](toolchains.md#bundler-aliases) for what that means for\nTypeScript, since the alias is invisible to the type checker.',
1272
1546
  },
1273
1547
  {
1274
- doc: "docs/api.md",
1275
- heading: "FlatList",
1276
- text: "FlatList GTK implementation: windowed core on ScrollView. Supported: virtualization (`estimatedItemSize` or `getItemLayout`, **`windowSize`/`initialNumToRender` — the primary scroll-performance knobs**, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`), `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`, `CellRendererComponent` (RN's per-cell wrapper — the list still hands it the cell's absolute `style` and the `onLayout` that measures it, and both must be applied; this is what `react-native-draggable-flatlist` builds its whole design on), `ListHeader/Footer/EmptyComponent`, `onEndReached(-Threshold)`, `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`), `inverted` (RN chat semantics: opens at `data[0]`, stays pinned on prepend), `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`, ref: `scrollToIndex`/`scrollToItem`/`scrollToOffset` + `scrollTo`/`scrollToEnd` (`FlatListHandle`) the SCROLL half of a ScrollView ref, not the geometry half: a windowed list is a composite over a ScrollView and owns no widget of its own, so a `measure()` here would have to pick some inner widget and pretend it was the list. Measure the `ScrollView` or a cell. Differences from RN: 1000 rows mount windowed in ~120 ms (v1 full mount was 879 ms); `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure and a wider window means fewer mount+reflow bursts per scrolled pixel (measured: −21% churn, late frames 10/s 7.7/s); rows beyond the visible ones are mounted `maxToRenderPerBatch` (10) at a time every `updateCellsBatchingPeriod` (50) ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one; no pull gesture — `onRefresh` must be app-triggered; an inverted list shorter than its viewport anchors to the top, not the bottom; `CellRendererComponent` is not applied to a STICKY cell (`stickyHeaderIndices`) pinning reorders the cell's real GTK widget, so the sticky container has to BE the cell",
1548
+ doc: "docs/guide/first-app.md",
1549
+ heading: "How it works",
1550
+ text: "```\nyour code (react-native API)\n └─ vite preset: aliases react-native react-native-gtkx, platform\n extensions .linux.tsx .native.tsx base\n └─ react-native-gtkx: Yoga (WASM) computes flexbox; styles are split into\n layout (Yoga) and visual (GTK CSS); coordinates are applied to\n real GTK widgets\n └─ gtkx: React reconciler GTK4 via FFI\n```\n\nThe Metro path draws the same picture with the Metro preset instead of\nthe vite one in the first step see [Toolchains](toolchains.md) for how\nthe two differ.",
1277
1551
  },
1278
1552
  {
1279
- doc: "docs/api.md",
1280
- heading: "SectionList",
1281
- text: "SectionList GTK implementation: on top of FlatList. Supported: `sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`). Differences from RN: viewability props are not exposed (section-aware ViewTokens pending)",
1553
+ doc: "docs/guide/first-app.md",
1554
+ heading: "Hello, GNOME",
1555
+ text: 'The entry point looks exactly like a React Native entry:\n\n```tsx\nimport { AppRegistry, StyleSheet, Text, View } from "react-native"\n\nconst App = () => (\n <View style={styles.screen}>\n <Text style={styles.title}>Hello, GNOME!</Text>\n </View>\n)\n\nconst styles = StyleSheet.create({\n screen: { flex: 1, alignItems: "center", justifyContent: "center" },\n title: { fontSize: 24, fontWeight: "700" },\n})\n\nAppRegistry.registerComponent("app", () => App)\nAppRegistry.runApplication("app", { title: "My App", width: 800, height: 600 })\n```\n\n`runApplication` accepts desktop parameters (`title`, `width`, `height`)\n— the only extension over the React Native signature.',
1282
1556
  },
1283
1557
  {
1284
- doc: "docs/api.md",
1285
- heading: "VirtualizedList",
1286
- text: "VirtualizedList GTK implementation: the same windowed core. Supported: RN's data-source shape over the list `FlatList` already sits on: `data` is OPAQUE and read only through `getItemCount(data)` and `getItem(data, index)`, both honoured LAZILY (only the rows the window mounts are ever asked for). Everything else is `FlatList`'s row above, `CellRendererComponent` included. Differences from RN: The accessors are OPTIONAL here and required upstreamone component serves both shapes, which is why `FlatList` needs no separate implementation. `scrollToItem` scans the source through `getItem` (upstream scans too; an opaque source has no index to ask). Every difference in the `FlatList` row applies unchanged",
1558
+ doc: "docs/guide/first-app.md",
1559
+ heading: "Running the app",
1560
+ text: "A project started from the template runs with:\n\n```bash\nnpm run dev\n```\n\nAn app that added Linux to an existing React Native project runs\n`run-linux` next to the platforms it already had:\n\n```bash\nnpx react-native run-linux # release bundle\nnpx react-native run-linux --dev # Metro dev server + Fast Refresh\n\n# the platforms this app already had\nnpx react-native run-ios\nnpx react-native run-android\n```\n\n`run-linux` ensures the gtkx codegen store, bundles with Metro for\n`--platform linux`, and opens the window. With `--dev` it starts (or\nreuses) the Metro dev server; edits apply to the live window with\ncomponent state preserved, syntax errors print readably in the terminal,\nand the app recovers on the next successful build.\n\n**Ctrl+Shift+D** the react-native-windows shortcut, the desktop\nstand-in for the shake gesture opens the Dev Menu: Reload, plus any\nentries the app registers through `DevSettings.addMenuItem`.\n\n`run-linux` always runs what it builds. For a release build that stops\nshort of opening a window packaging, CI, handing a bundle to someone\nelse's machine see [Packaging](packaging.md) for `build-linux` instead.",
1287
1561
  },
1288
1562
  {
1289
- doc: "docs/api.md",
1290
- heading: "Switch",
1291
- text: "SwitchGTK implementation: GtkSwitch. Supported: `value`/`onValueChange`, `disabled`. Differences from RN: sized by the GTK theme, not iOS metrics",
1563
+ doc: "docs/guide/first-app.md",
1564
+ heading: "The gallery: a tour of the platform",
1565
+ text: "`examples/gallery` in the repository is every capability this platform\nclaims, one per sidebar entry, in an app you run and poke at:\n\n```bash\nnpm install # from the repo root (workspaces)\ncd examples/gallery\nnpm run dev # gtkx dev vite + Fast Refresh\n```\n\nThe chrome is the package's own sidebar navigator — a native\n`Adw.NavigationSplitView` with the sections in a real `GtkListBox` —\ngrouped in the order a reader meets the platform:\n\n- **React Native** — views, text and layout, clipping, inputs/buttons/\n toggles, lists and media, Modal, Animated, interpolation, transforms,\n gestures (the responder system and `PanResponder`), and the core APIs\n (`Platform`, `Dimensions`, `Appearance`, `Alert`, `Linking`);\n- **gtkx** widget hosting (React Native content inside a GTK widget's\n child and slots) and the Adwaita stack (`Adw.NavigationView` driven\n declaratively);\n- **Modules** — Reanimated (values, motion, layout animations, and the\n measured boundary of what's driven off the render thread), gesture\n handler (pan/tap/long-press, pinch/rotation, cross-gesture relations),\n drag-and-drop, Svg, and three sections running the real upstream\n `react-native-reanimated-dnd` and `react-native-drawer-layout`\n packages unmodified.\n\n`GALLERY_SECTION=<id>` opens one section directly; `GALLERY_SCHEME=light`\nstarts in the light theme (the HeaderBar button toggles either way\nlive).",
1292
1566
  },
1293
1567
  {
1294
- doc: "docs/api.md",
1295
- heading: "ActivityIndicator",
1296
- text: "ActivityIndicatorGTK implementation: GtkSpinner. Supported: `animating`, `size` (small/large/number). Differences from RN: no `color` yet",
1568
+ doc: "docs/guide/first-app.md",
1569
+ heading: "Other examples in the repository",
1570
+ text: "- `examples/profile` a static layout; the same source also builds with\n react-native-web (`examples/profile-web`);\n- `examples/rn-app` — a cli-init React Native app with ios, android and\n linux side by side;\n- `examples/hn-app` a Hacker News reader on the Metro path: live API\n data over Node `fetch`, state-based two-screen navigation, a lazily\n loaded comment tree.",
1297
1571
  },
1298
1572
  {
1299
- doc: "docs/api.md",
1300
- heading: "Modal",
1301
- text: "Modal GTK implementation: modal GtkWindow (portal). Supported: `visible`, `onRequestClose` (Escape/close button), `title`, `width`/`height`; independently resizable with relayout. Differences from RN: desktop semantics: a separate window, not an overlay; `transparent`/`animationType` are no-ops",
1573
+ doc: "docs/guide/toolchains.md",
1574
+ heading: "The vite path",
1575
+ text: "`gtkx dev` and `gtkx build` are the two commands a vite-path project\nruns day to day; both start vite themselves and pick up the project's\n`vite.config.ts` (with the `react-native-gtkx/vite` preset applied)\nautomatically.\n\n```bash\ngtkx dev [entry] # dev server + Fast Refresh; entry defaults to\n # src/index{.tsx,.jsx,.ts,.js}\ngtkx build [entry] # production bundle\n```\n\nBoth accept `--cwd=<path>` to run against a project root other than the\ncurrent directory. `gtkx build` also accepts `--asset-base=<path>`, an\nasset base path relative to the executable's directory, for layouts\nwhere the bundle and its assets don't sit side by side. `gtkx codegen`\n(with `--force` to wipe and regenerate a corrupted store) generates the\n`@gtkx/gi`/`@gtkx/jsx` bindings for the GIR libraries declared in\n`gtkx.config.ts` — `gtkx dev`/`gtkx build` run it automatically; a bare\n`vitest run` does not (see [Testing](#testing) below).",
1302
1576
  },
1303
1577
  {
1304
- doc: "docs/api.md",
1305
- heading: "Animated.View",
1306
- text: "Animated.View GTK implementation: direct widget calls. Supported: `opacity` and the whole `transform` array `translateX/Y`, `scale`, `scaleX`, `scaleY`, `rotate`/`rotateZ` — driven by Animated nodes, bypassing React (an angle comes from `interpolate` with a `deg`/`rad` outputRange); `top`/`left`/`right`/`bottom` too when the node's own `position` is `\"absolute\"`, which is what makes `Animated.ValueXY`'s `getLayout()` work; and `width`/`height` where the change is confined to the node that owns it; plus the same responder and touch props `View` takesthis is where an idiomatic `PanResponder` drag lands; plus `pointerEvents` and `animatedProps`, because Reanimated's `Animated.View` is `createAnimatedComponent(View)` and every View prop reaches it there. Differences from RN: `rotateX`/`rotateY`/`perspective` (3D), `skewX`/`skewY` and `matrix` are not supported, and the transform origin is always the view's centre (no `transformOrigin`)",
1578
+ doc: "docs/guide/toolchains.md",
1579
+ heading: "The Metro path",
1580
+ text: "`run-linux`, `build-linux` and `deploy-linux` are contributed to the React\nNative CLI by `react-native-gtkx`'s own `react-native.config.js` — no\nseparate install, they come with the package.\n\n```bash\nnpx react-native run-linux [--entry-file <path>] [--bundle-output <path>]\n [--skip-bundling] [--dev] [--port <number>]\n\nnpx react-native build-linux [--entry-file <path>] [--bundle-output <path>]\n [--standalone] [--sea] [--sea-output <path>]\n\nnpx react-native deploy-linux [--entry-file <path>] [--target <formats>]\n [--out <path>] [--print-manifests] [--skip-build]\n```\n\n`run-linux` bundles with Metro and opens the window; `--dev` starts (or\nreuses) the Metro dev server on `--port` (default `8081`) instead, for\nFast Refresh. `build-linux` bundles for distribution and stops short of\nrunning itsee [Packaging](packaging.md) for `--standalone`/`--sea`,\nthe two flags that turn the Metro bundle into something shippable.\n`deploy-linux` goes one step further and builds an installable `.deb`/\n`.rpm`/`.AppImage` from that same Metro build also covered on the\n[Packaging](packaging.md) page, which is where its vite-path counterpart\nlives too (the same command name works for either toolchain).",
1307
1581
  },
1308
1582
  {
1309
- doc: "docs/api.md",
1310
- heading: "SafeAreaView",
1311
- text: "SafeAreaViewGTK implementation: = View. Supported: —. Differences from RN: no notches on desktop",
1583
+ doc: "docs/guide/toolchains.md",
1584
+ heading: "The React Compiler (vite path only)",
1585
+ text: "`gtkx dev` and `gtkx build` run the [React\nCompiler](https://react.dev/learn/react-compiler) over every source file\nin the project never `node_modules`. It is on unless `gtkx.config.ts`\nturns it off:\n\n```ts\nexport default defineConfig({\n libraries: [\"Gtk-4.0\", \"Adw-1\"],\n applicationId: \"com.example.myapp\",\n reactCompiler: false,\n})\n```\n\nOmitting the option and setting it to `true` mean the same thing — only\nan explicit `false` disables it. The Metro path (`run-linux`/\n`build-linux`) keeps the app's stock Babel preset and never runs the\ncompiler.\n\nIf a ported app misbehaves in a way that smells like stale rendering —\na value that should have updated didn't — set `reactCompiler: false` and\nsee whether the symptom goes away. That one line tells you which half of\nthe system to debug: if the symptom disappears, the underlying issue is\na [Rules of React](https://react.dev/reference/rules) violation the\ncompiler is compiling around correctly, and the real fix is moving the\noffending read into state, a ref, or a hook — not leaving the compiler\noff. A typical shape: a component reading mutable module-level state\nduring render has that read memoized by the compiler, so it renders\nfourteen times and shows the same value every time — it looks like a\nbroken counter and is in fact a working one behind a cached render.\n\nOn React Native the compiler is opt-in, so an app that doesn't follow the\nRules of React still works there. Here it's on by default, so the same\nviolations become visible misbehavior on a platform where everything\nelse is also new — which reads as a platform bug when it's a Rules-of-\nReact one.\n\nReanimated shared values have their own spelling for the same reason:\n`sharedValue.value = x` and `sharedValue.set(x)` both work, but only\n`.get()`/`.set()` passes compiler-aware lint (`react-hooks/immutability`\ntreats anything a hook returns as frozen). Prefer `.get()`/`.set()` in\nnew code; `.value` keeps working, so a ported app doesn't need a\nrewrite.",
1312
1586
  },
1313
1587
  {
1314
- doc: "docs/api.md",
1315
- heading: "StatusBar",
1316
- text: "StatusBar GTK implementation: null. Supported: —. Differences from RN: no status bar",
1588
+ doc: "docs/guide/toolchains.md",
1589
+ heading: "Bundler aliases",
1590
+ text: 'Both presets rewrite the same six package names: `react-native` itself,\nplus `react-native-svg`, `react-native-reanimated`,\n`react-native-worklets`, `react-native-gesture-handler` and\n`react-native-reanimated-dnd` onto their compat subpaths. The\n`react-native-svg` alias is the one most projects notice at the type\nlevel: the alias is a bundler-time rewrite, so TypeScript still needs\nits own answer for the bare `"react-native-svg"` specifier — an\nunresolved import in the editor even though the build works. Which fix\napplies depends on the project:\n\n- **Also ships to iOS/Android/web**: install the real\n `react-native-svg` — the app needs it there regardless, and it ships\n its own `.d.ts`. The Linux build never executes that package\'s code\n (the alias rewrites the import before Node sees it).\n- **Linux-only project** (the template, or an app with no mobile\n target): install `react-native-svg` as a **devDependency purely for\n its types** — `npm install -D react-native-svg`. The ordinary fix for\n a bundler-alias setup with no real package installed.',
1317
1591
  },
1318
1592
  {
1319
- doc: "docs/api.md",
1320
- heading: "Root",
1321
- text: "Root GTK implementation: internal root. Supported: `width`/`height`. Differences from RN: extension: required by the test harness",
1593
+ doc: "docs/guide/toolchains.md",
1594
+ heading: "Testing",
1595
+ text: 'react-native-gtkx ships its GTK component-testing recipe as two\nsubpaths, so a consumer app doesn\'t have to rediscover it:\n\n- `react-native-gtkx/vitest` — `reactNativeGtkxTest()`, a ready Vitest\n project config: the headless-compositor plugin, the `react-native`\n alias and platform extensions, and the React act-environment setup;\n- `react-native-gtkx/testing` — re-exports `@gtkx/testing`\'s\n render/screen/userEvent/fireEvent surface, plus `renderHookWithWindow`\n for hooks that read the active window (`useWindowDimensions` and\n similar).\n\n```ts\nimport { reactNativeGtkxTest } from "react-native-gtkx/vitest"\nimport { defineConfig } from "vitest/config"\n\nexport default defineConfig(reactNativeGtkxTest())\n```\n\nThe default test glob is `**/*.gtk.test.{ts,tsx}`. Component tests need a\nheadless Wayland compositor and D-Bus on `PATH` — `sway xwayland dbus` on\nUbuntu — and `gtkx codegen` must already have generated the project\'s\n`@gtkx/gi` bindings, since a bare `vitest run` doesn\'t trigger codegen\nitself the way `gtkx dev`/`gtkx build` do; wire that as a `pretest`\nscript. A missing compositor fails a test run with a readable error\nrather than hanging.\n\n```tsx\nimport { Root } from "react-native"\nimport { render, screen } from "react-native-gtkx/testing"\n\n// react-native-gtkx components need a layout root — AppRegistry.runApplication()\n// in the real app, <Root> in a test.\nawait render(\n <Root\n width={800}\n height={600}\n >\n <App />\n </Root>,\n)\nexpect(screen.getByText("Hello, GNOME!")).toBeTruthy()\n```',
1322
1596
  },
1323
1597
  {
1324
- doc: "docs/api.md",
1325
- heading: "NestedRoot",
1326
- text: "NestedRoot GTK implementation: internal root. Supported: —. Differences from RN: extension: a Yoga root inside any GTK container slot (navigation pages, custom containers); the slot allocation is the viewport",
1598
+ doc: "docs/guide/toolchains.md",
1599
+ heading: "MCP server for agents",
1600
+ text: 'An agent working inside a project that depends on react-native-gtkx can\nask the library about itself instead of guessing:\n`react-native-gtkx-mcp` is a [Model Context\nProtocol](https://modelcontextprotocol.io) server shipped as a `bin` on\nthe package. Register it project-level (Claude Code\'s `.mcp.json`, or\nthe equivalent config of any MCP-compatible client):\n\n```json\n{\n "mcpServers": {\n "react-native-gtkx": { "command": "npx", "args": ["react-native-gtkx-mcp"] }\n }\n}\n```\n\n`npx react-native-gtkx-mcp` from the project root resolves the locally\ninstalled version, so it always answers for the exact react-native-gtkx\nversion the project has. Three tools:\n\n- `rn_gtkx_list_surface` browse the surface without knowing a name\n first (portable components/APIs, gtk/adw widgets, common), with\n counts;\n- `rn_gtkx_describe_component` — the one to reach for first: does a\n component/widget exist, which subpath it\'s exported from, what GTK\n widget backs it, what differs from React Native, whether it takes\n `style`/`onLayout` or is raw;\n- `rn_gtkx_search_docs` — free-text fallback for symptoms and\n known-issue questions the other two can\'t answer by name.\n\nIt runs without GTK installed — no `@gtkx/*` import anywhere in it,\nreading only the package\'s own bundled docs/manifest data. That matters\nin practice: an agent is often reading the project from a machine with\nno GTK toolchain at all.',
1327
1601
  },
1328
1602
  {
1329
- doc: "docs/api.md",
1330
- heading: "IntrinsicRoot",
1331
- text: "IntrinsicRoot GTK implementation: internal root. Supported: —. Differences from RN: extension: a content-sized Yoga root for chrome slots (HeaderBar start/end)reports its content size to GTK",
1603
+ doc: "docs/guide/plain-gtk.md",
1604
+ heading: "Choosing the profile",
1605
+ text: 'Drop `"Adw-1"` from `gtkx.config.ts`\'s `libraries`:\n\n```ts\nimport { defineConfig } from "@gtkx/config"\n\nexport default defineConfig({\n libraries: ["Gtk-4.0"],\n applicationId: "com.example.myapp",\n})\n```\n\nAn app configured this way never links libadwaita: no Adwaita theming,\nno `Adw.StyleManager`, no Adwaita widgets. The choice is per-app, made\nonce, at the `gtkx.config.ts` level there\'s no runtime flag to flip\nbetween the two profiles in a single build.\n\nreact-native-gtkx\'s own bridge is split to make this possible: a core\nmodule with zero Adw imports covers everything `View`/`Text`/\n`ScrollView`/`Modal`/`Animated`/gestures/`FlatList` and the rest of the\nportable surface need, and a separate Adw module is loaded only when the\napp\'s codegen store actually has Adw bindings.',
1332
1606
  },
1333
1607
  {
1334
- doc: "docs/api.md",
1335
- heading: "StyleSheet",
1336
- text: "StyleSheet Supported: `create`, `flatten`, `compose`, `absoluteFill(Object)`, `hairlineWidth`. Differences: —",
1608
+ doc: "docs/guide/plain-gtk.md",
1609
+ heading: "What still works",
1610
+ text: 'Three parts of the API fall back to a plain-GTK equivalent, so the same\napp code runs on both profiles without branching on which one it got:\n\n- **`AppRegistry`**\'s `chrome: "content"` falls back to the plain\n `GtkApplicationWindow` chrome that `chrome: "system"` always uses,\n instead of throwing. This is why requesting `chrome: "content"`\n unconditionally is the right default for a portable app: HeaderBar-as-\n chrome where Adw exists, an ordinary window chrome where it doesn\'t,\n with no `if (adwAvailable())` branch of the app\'s own to write.\n `breakpoints` degrades the same way `chrome: "content"` under the\n wrong chrome always has accepted, ignored, one dev warning — naming\n `"Adw-1"` as the reason instead of the chrome mismatch.\n- **`Alert.alert`** falls back to `Gtk.AlertDialog` (GTK ≥ 4.10),\n preserving button order, default/cancel mapping and callbacks. Lost:\n `destructive`/`isPreferred` appearance, and `cancelable: false` with no\n cancel-style button.\n- **`Appearance`/`useColorScheme`** fall back to the\n `org.freedesktop.appearance` desktop portal\'s `color-scheme` setting\n (with live updates via its `SettingChanged` signal), then to\n `Gtk.Settings:gtk-application-prefer-dark-theme` when no portal\n answers. The contract is identical either way: always `"light"`/\n `"dark"`, change events still fire, and `setColorScheme` is local to\n the process on both profiles, never a system-wide write.',
1337
1611
  },
1338
1612
  {
1339
- doc: "docs/api.md",
1340
- heading: "PlatformColor",
1341
- text: 'PlatformColor Supported: Adwaita variables: `PlatformColor("accent-bg-color")` `var(--...)`, `@named`. Differences: names are Adwaita, not iOS/Android',
1613
+ doc: "docs/guide/plain-gtk.md",
1614
+ heading: "What refuses",
1615
+ text: 'Two subpaths need Adw unconditionally and refuse to import without it:\n`react-native-gtkx/adw` (the Adwaita widget bindings) and\n`react-native-gtkx/navigation` (built on `AdwHeaderBar`/\n`Adw.NavigationView`). Importing either on the plain-GTK profile throws,\nnaming the fix:\n\n```\n[react-native-gtkx] "@gtkx/jsx/adw" requires "Adw-1" in this app\'s\ngtkx.config.ts `libraries` see the plain-GTK profile documentation for\nwhat needs Adw unconditionally and what falls back without it.\n```\n\n`react-native-gtkx/common`\'s `NavigationStack`/`NavigationStackPage` are\nthe same story at component granularity rather than import granularity:\nthe subpath itself imports fine (its barrel also carries Adw-free\nexports — `Widget`, `Icon`, `SlotContent`), and only actually\n**rendering** one of these two throws, naming the component instead of\nthe raw specifier. There\'s no fallback to degrade to for either one —\nthis platform ships no non-Adwaita stand-in for `Adw.NavigationView`.',
1342
1616
  },
1343
1617
  {
1344
- doc: "docs/api.md",
1345
- heading: "AppRegistry",
1346
- text: 'AppRegistry Supported: `registerComponent`, `runApplication(appKey, {title,width,height,initialProps,chrome,actionAccels,breakpoints,applicationActions,windowActions,windowControllers})`, `getAppKeys`. Differences: desktop window parameters; `chrome: "content"` uses an AdwApplicationWindow with no window titlebar when this app declares `"Adw-1"` the app\'s HeaderBars (navigation) become the chrome and falls back to the plain `GtkApplicationWindow` `chrome: "system"` already uses when it does not (see [Plain GTK profile](#plain-gtk-profile) below): request `chrome: "content"` unconditionally and get HeaderBar-as-chrome where Adw exists, an ordinary window otherwise, with no branching of the app\'s own. `actionAccels` binds accelerators to action names on the `GtkApplication`; `breakpoints` reaches `AdwApplicationWindow`\'s own prop and only does anything under `chrome: "content"` WITH `"Adw-1"` declared otherwise one of two dev warnings fires instead, each at most once per app run: `chrome: "system"` names the wrong chrome, `chrome: "content"` with no `"Adw-1"` names that instead. **`applicationActions`/`windowActions`/`windowControllers` are deprecated** reach for [`<ApplicationActions>`/`<WindowActions>`/`<WindowControllers>`](platform-layer.md#actions-and-shortcuts-declared-in-the-app-tree) instead; they still work unchanged',
1618
+ doc: "docs/guide/plain-gtk.md",
1619
+ heading: "How the profile is detected",
1620
+ text: 'Nothing in application code needs to check which profile it\'s running\non the three fallbacks above and the two refusals handle it — but the\nmechanism is worth knowing when debugging a build: the platform asks a\nsingle question, "does this app\'s codegen store actually have Adw\nbindings", answered differently on each toolchain:\n\n- On the Metro/SEA host, the run-linux host resolves every module\n (including the Adw ones, when declared) into a global registry before\n the bundle ever runs; a plain-GTK app simply has no Adw entry there,\n which is the expected, normal shape rather than a broken install.\n- On the vite path (`gtkx dev`/`gtkx build`), a build-time constant is\n set from whether `@gtkx/gi/adw` actually resolves out of the app\'s own\n `node_modules` decided once, when vite starts.\n- Anywhere else (a bare `vitest` project, for instance), a dynamic-\n import probe answers the same question at runtime.\n\nAll three answer the one question the bridge asks internally —\n"is Adw available in this app"consistently, regardless of which\ntoolchain built the app.',
1347
1621
  },
1348
1622
  {
1349
- doc: "docs/api.md",
1350
- heading: "Platform",
1351
- text: 'PlatformSupported: `OS: "linux"`, `Version` (GTK), `select` (linux native default), `isTV`, `isTesting`. Differences:',
1623
+ doc: "docs/guide/packaging.md",
1624
+ heading: "The vite path: one bundle",
1625
+ text: "```bash\ngtkx build\n```\n\nproduces `dist/bundle.mjs` everything except the native GTK addon\ninlined into one file — plus `dist/gtkx.node` (and\n`dist/gschemas.compiled` alongside it, if the app declares a GSettings\nschema; the bundle's own banner points `GSETTINGS_SCHEMA_DIR` at its own\ndirectory). That pair is the whole runtime: copy it anywhere with Node\n≥ 24, GTK4 ≥ 4.20 and libadwaita ≥ 1.8 (or just GTK4, on the [plain-GTK\nprofile](plain-gtk.md)) and `node bundle.mjs` runs it no `node_modules`\ninvolved.",
1352
1626
  },
1353
1627
  {
1354
- doc: "docs/api.md",
1355
- heading: "Dimensions",
1356
- text: 'DimensionsSupported: `get("window"/"screen")`, `addEventListener("change")`. Differences: main window only (transient windows are ignored)',
1628
+ doc: "docs/guide/packaging.md",
1629
+ heading: "The Metro path: `build-linux`",
1630
+ text: "```bash\nnpx react-native build-linux\n```\n\nwrites `dist/main.jsbundle` and stops the release counterpart to\n`run-linux` that iOS, Android and react-native-windows already have.\nUnlike the vite bundle, this is **not** self-contained: Metro\ndeliberately keeps `@gtkx/*`, `react` and `yoga-layout` out of the\nbundle, since they have to be the exact instances the Node+GTK host\nloads, not a second copy Metro inlines. Running `dist/main.jsbundle`\nlater needs, on top of Node/GTK/libadwaita, a real `node_modules` with\n`react-native-gtkx` installed and the app's `gtkx.config.ts` at the\nworking directory:\n\n```bash\nnode node_modules/react-native-gtkx/dist/runner/host.js dist/main.jsbundle\n```\n\nThat's a fine way to run a release bundle from a checkout — any ordinary\n`npm install` of the app already has that `node_modules` — but a bad\nthing to ship: the closure is not the handful of runtime modules it\nsounds like. Packaging it that way once measured **10,515 files, 206\nMiB installed** to run a 369 KB bundle, because `react-native-gtkx`'s\ninstall drags its whole build toolchain along. `--standalone` and\n`--sea` exist to remove that closure entirely.",
1357
1631
  },
1358
1632
  {
1359
- doc: "docs/api.md",
1360
- heading: "useWindowDimensions",
1361
- text: "useWindowDimensions Supported: reactive main-window dimensions. Differences: —",
1633
+ doc: "docs/guide/packaging.md",
1634
+ heading: "Choosing an artifact",
1635
+ text: "`build-linux` produces three shapes from the same Metro step; the choice\nbetween them is a distribution question, not a different build:\n\n| Flag | Artifact | Needs installed | Size (`hn-app`, linux-arm64) |\n| -------------- | -------------------------- | ---------------------------------- | ---------------------------- |\n| _(none)_ | `dist/main.jsbundle` | a `node_modules` tree **and** Node | 0.4 MB + the tree |\n| `--standalone` | `dist/<name>.cjs` | Node only | 6.9 MB |\n| `--sea` | `dist/<name>` (executable) | nothing at all | 104 MB (30 MB compressed) |\n\n```bash\nnpx react-native build-linux --standalone # in the app root\nnode ./dist/<your-package-name>.cjs # one script, system node\n\nnpx react-native build-linux --sea\n./dist/<your-package-name> # one executable, nothing else\n```\n\nBoth flags produce the jsbundle exactly as before, then one additional\nfile next to it. `--sea-output <path>` overrides where that file goes;\nthe default is `dist/<package name>` with any npm scope stripped (plus\n`.cjs` for `--standalone`).\n\n`--standalone` is what `deploy-linux` (below) builds a `.deb`/`.rpm`/\n`.AppImage` from — it is not really a competing choice, it is that\npackaging step's own input. Reach for it directly only when you want the\nbundle without any packaging around it (a container image, a CI artifact\npassed to another step). `--sea` stays the odd one out: \"download this\none file and run it,\" where nothing not even a package manager — can\nbe assumed. It's `--standalone` with a copy of Node wrapped around it,\nand that copy is the entire ~97 MB difference between the two.",
1362
1636
  },
1363
1637
  {
1364
- doc: "docs/api.md",
1365
- heading: "Appearance",
1366
- text: "Appearance Supported: `getColorScheme`, `setColorScheme` (AdwStyleManager; on the plain-GTK profile no `Adw-1`sourced from the `org.freedesktop.appearance` `color-scheme` desktop-portal setting instead, with live updates via its `SettingChanged` signal, falling back to `Gtk.Settings:gtk-application-prefer-dark-theme` when no portal answers), `addChangeListener`. Differences: On the plain-GTK profile, `setColorScheme` is local-only exactly like the Adw path neither ever writes a SYSTEM-wide preference (`AdwStyleManager` and the fallback `Gtk.Settings` toggle are both per-process only; the portal itself has no write API, by design). With no portal reachable and no explicit `setColorScheme` call yet, the reported scheme is whatever `Gtk.Settings` already defaults to (light) rather than any observed system value",
1638
+ doc: "docs/guide/packaging.md",
1639
+ heading: "What `--sea`/`--standalone` need that a plain build doesn't",
1640
+ text: "Both flags inline `virtual:gtkx-config` (which re-exports codegen\noutput), sounlike a plain `build-linux`, which needs neither they\nneed the gtkx codegen store already generated, and therefore GTK\ndevelopment headers on the build machine. The `--sea` build also fetches\n`postject` through `npx` the first time it runs, so that first build\nneeds network access.\n\nThe native addon (a real `dlopen`ed library) can't be plain bundled JS,\nso both artifacts carry it as bytes instead — a SEA asset in the\nexecutable, a base64 literal in the `.cjs` — and extract it to\n`$XDG_CACHE_HOME/react-native-gtkx-sea` on first run, keyed by content\nhash (falling back to a temp directory if `$HOME` is read-only). Repeat\nlaunches reuse the extracted file.\n\nThe `--sea` executable is large mostly because it carries a full copy of\nNode: on linux-arm64, `hn-app` measures 104 MB (30 MB zstd-compressed),\nof which roughly 98 MB is Node itself the app code and native addon\ntogether are under 7 MB. The build strips Node's own debug symbols as\npart of assembling the executable (best-effort: a build machine without\n`binutils` gets a larger executable and a warning, not a failed build),\nwhich is most of what keeps that number from being worse — an\nunstripped `node` binary carries about 19 MB of debug information\nnothing in a shipped app can use.",
1367
1641
  },
1368
1642
  {
1369
- doc: "docs/api.md",
1370
- heading: "useColorScheme",
1371
- text: "useColorScheme Supported: reactive theme. Differences: —",
1643
+ doc: "docs/guide/packaging.md",
1644
+ heading: "Packaging: `npx react-native deploy-linux`",
1645
+ text: "One command builds real `.deb`/`.rpm`/`.AppImage`/`.flatpak` packages —\nvalidated desktop entry and AppStream metadata, correct per-format\ndependencies, an icon installed into the system theme — from either\ntoolchain. It works from any react-native-gtkx app's own project\ndirectory; which half of it runs is decided for you:\n\n- a project with a `vite.config.*` at its root (the vite path — gallery,\n monitor and every app built with `gtkx dev`/`gtkx build`) proxies\n straight to `npx gtkx deploy`, since that project already looks\n exactly like what `gtkx deploy` expects;\n- everything else (the Metro path — an app with `ios`/`android` and\n `run-linux`/`build-linux`) runs a real `--standalone` build first (the\n same step described above), reshapes that output into the same shape a\n vite build would have produced, then hands off to `gtkx deploy --skip-build`\n over it. hn-app is this repo's own example of that path.\n\n```bash\nnpx react-native deploy-linux --target deb,rpm,appimage\nnpx react-native deploy-linux --target deb --print-manifests # metadata only, no packages built\nnpx react-native deploy-linux --skip-build --target rpm # package what was already built\n```\n\n| Flag | Meaning |\n| --------------------- | ---------------------------------------------------------------------- |\n| `--entry-file <path>` | Metro path only — same meaning as `build-linux`'s (default `index.js`) |\n| `--target <formats>` | Comma-separated: `deb`, `rpm`, `appimage`, `flatpak` |\n| `--out <path>` | Output directory, relative to the project root (default `build`) |\n| `--print-manifests` | Write the desktop-entry/AppStream files, then stop no packages built |\n| `--skip-build` | Package the already-staged `dist/` instead of rebuilding (see below) |\n\n`--skip-build` means the same thing on both paths — package what's\nalready in `dist/`, don't rebuild — but on the Metro path it also skips\nthis command's own build/staging step, not just `gtkx build`'s; running\nit before anything has ever been staged is a clear error, not a\nconfusing one three layers down.",
1372
1646
  },
1373
1647
  {
1374
- doc: "docs/api.md",
1375
- heading: "AppState",
1376
- text: "AppState Supported: `currentState` active/background, `addEventListener`. Differences: driven by the window's `is-active`",
1648
+ doc: "docs/guide/packaging.md",
1649
+ heading: "The `deploy` config",
1650
+ text: 'Both toolchains read the same block from the project\'s `gtkx.config.ts`:\n\n```ts\nexport default defineConfig({\n applicationId: "com.example.myapp",\n libraries: ["Gtk-4.0", "Adw-1"],\n deploy: {\n name: "My App",\n summary: "One line describing the app",\n description: "A longer paragraph or two.",\n categories: ["Utility"],\n developer: { name: "Your Name", email: "you@example.com" },\n license: "MIT",\n icons: "icon.svg",\n },\n})\n```\n\nThere is no default: deploying without a `deploy` block is a thrown\nerror naming exactly which fields to add (derived from `package.json`\nwhere it can guess). `name`/`summary`/`categories` feed a real\n`.desktop` entry and AppStream `.metainfo.xml`, validated by\n`desktop-file-validate`/`appstreamcli` before anything is packaged — a\ncategory that isn\'t a real freedesktop one, or two main categories where\nonly one is allowed, fails the build rather than shipping quietly wrong\nmetadata. The full schema (screenshots, MIME types, per-target\ndependencies, signing, `targets`/`binaryName`/`applicationId` overrides)\nis [gtkx\'s own reference](https://gtkx.dev/guide/deploying); this page\ncovers what\'s specific to running it through `deploy-linux`.',
1377
1651
  },
1378
1652
  {
1379
- doc: "docs/api.md",
1380
- heading: "Alert",
1381
- text: "AlertSupported: `alert(title, message, buttons, options)` Adw.AlertDialog (`Gtk.AlertDialog`, GTK 4.10, on the plain-GTK profile no `Adw-1`). Differences: `cancel`/`destructive`/`isPreferred` styles. On the plain-GTK profile, `destructive`/`isPreferred` appearance (Adw's `ResponseAppearance`) is lost — `Gtk.AlertDialog` has no equivalent, every button renders the same; default/cancel mapping is preserved. `cancelable: false` with no `cancel`-style button cannot be honored there either: `Gtk.AlertDialog` has no way to block Esc/window-close dismissal the way `Adw.AlertDialog.setCanClose(false)` does add a `cancel`-style button for identical behavior on both profiles",
1653
+ doc: "docs/guide/packaging.md",
1654
+ heading: "What ships",
1655
+ text: 'Every target bundles its own private copy of Node by default\n(`deploy.node.source: "download"`, verified against `SHASUMS256.txt`,\ncached under `~/.cache/gtkx/node/`) no `Depends: nodejs` line, at the\ncost of roughly 97 MB per package. Measured on linux-arm64, both\ntoolchains land in the same range because both are dominated by that\nbundled runtime: `monitor` (vite) is a 40.3 MB `.deb`; `hn-app` (Metro)\nis a 40.7 MB `.deb`, a 40.5 MB `.rpm` and a 36.5 MB `.AppImage`. A `.deb`\ndeclares:\n\n```\nDepends: libgtk-4-1, libadwaita-1-0, hicolor-icon-theme, adwaita-icon-theme, gsettings-desktop-schemas, libc6 (>= 2.28)\n```\n\n— the glibc floor read directly off the bundled Node\'s own ELF notes.\nPackage a [plain-GTK](plain-gtk.md) app the same way and `libadwaita-1-0`\ndrops out on its own (it comes from the app\'s declared `libraries`, not\na fixed list). Prefer the old thin-package shape, where the system\'s own\n`nodejs` is a dependency instead of a bundled copy? `deploy.node.source:\n"host"` (embed the machine\'s own Node) or `"path"` (embed a specific\none) are the escape hatch see the reference linked above.\n\n`deb`/`rpm`/`appimage` need nothing preinstalled beyond a handful of\nsmall system tools (`desktop-file-validate`, `appstreamcli`, `tar`,\n`file`, `binutils` optionally for a smaller bundled Node) — the actual\npackagers, [nfpm](https://github.com/goreleaser/nfpm) for deb/rpm and\n[appimagetool](https://github.com/AppImage/appimagetool), download and\ncache themselves under `~/.cache/gtkx` on first use. A missing tool\nfails with the exact `apt`/`dnf`/`pacman`/`zypper` command to install it,\ndetected from `/etc/os-release`. `flatpak` is the heavier target: it\nneeds `flatpak`/`flatpak-builder` installed and the GNOME `Platform`/\n`Sdk` runtime pulled from Flathub before it can build anything (a\none-time, network-bound, multi-hundred-MB fetch) — every example this\nproject ships deliberately leaves `flatpak` out of its own\n`deploy.targets`, proven separately as a build-only artifact instead of\nbeing part of the regular `deb,rpm,appimage` set.',
1382
1656
  },
1383
1657
  {
1384
- doc: "docs/api.md",
1385
- heading: "Linking",
1386
- text: 'Linking Supported: `openURL`, `canOpenURL` (http/https/mailto/file), `getInitialURL` (null), `addEventListener("url")`. Differences: system launcher; no deep-link delivery on desktop yet "url" subscriptions never fire',
1658
+ doc: "docs/guide/packaging.md",
1659
+ heading: "The vite path's own `--sea`",
1660
+ text: "There isn't one, and `deploy-linux` is why that's no longer the gap it\nused to be: the vite bundle loads the native addon through a dynamically\nobtained `require` a bundler can't intercept the way it intercepts a\nstatic import, and the bundle needs top-level await, which the\nsingle-file SEA format can't run — both investigated, neither a drop-in\nfix. Ship a vite-path app as its `bundle.mjs`/`gtkx.node` pair for\nrunning from a checkout, or package it with `deploy-linux` for\neverything else; the gap this used to be (no way to turn a vite app into\nsomething a package manager installs) is what `deploy-linux` closes.",
1387
1661
  },
1388
1662
  {
1389
- doc: "docs/api.md",
1390
- heading: "InteractionManager",
1391
- text: "InteractionManagerSupported: `runAfterInteractions(task?)` (cancellable, then-able), `createInteractionHandle`/`clearInteractionHandle`, `addListener`. Differences: navigation transitions register interactions, so screen work deferred with `runAfterInteractions` waits for the push/pop slide",
1663
+ doc: "docs/gtkx-1.2-notes.md",
1664
+ heading: "Live workarounds",
1665
+ text: '| Name | What 1.2.2 does | Our workaround | Removal condition |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `runtime-dedupe` | Two bundled copies of the gtkx runtime still double-init GLib and abort; nothing guards against it across two DISTINCT `.node` files. Reproduced on 1.2.2 by actually building the duplicate: `gtkx: GLib-ERROR: g_log_set_writer_func() called multiple times`. The FAILURE MODE changed since 1.0 (see upstream-gtkx.md ask #2) it is now a catchable, named Node `uncaughtException` instead of a SIGABRT/core dump — but the underlying defect has not: the init is still not idempotent across distinct binaries, and the error still does not name the duplicate package | `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits | Idempotent runtime init upstream across distinct native-addon files, or an error that names the duplicate |\n| `prop-portal` | `createPortal(children, container, key?)` is still byte-for-byte the rc.3/rc.4 signature and can still only target a container\'s DEFAULT slot ("children"). `@gtkx/react`\'s public entry and its `/internal` subpath both still withhold the `"gtkx:prop"` element — `/internal` still re-exports `createElementComponent` (the function that BUILDS an intrinsic element by GType name) but not the `Prop` literal itself, which stays a sibling export inside `element.js` that no public or `/internal` path reaches | `gtkx/bridge/slot-portal.ts` restates the `"gtkx:prop"` element name and wraps it in `createSlotPortal(children, target, slot)` — the one line that would move if gtkx renames it. `WindowActions`/`ApplicationActions`/`WindowControllers` are built on it | gtkx exports a slot-aware portal (or the prop element) from a public entry point |\n| `renderhook-no-window` | `renderHook` still mounts into a bare `Gtk.Box` — 1.2.2\'s `render-hook.js` still passes `container: new Gtk.Box()` unconditionally, and `RenderHookOptions` still carries only `wrapper` and `initialProps` — so window-dependent APIs have no toplevel to read | Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) | `renderHook` mounts into the same harness window `render` uses |\n| `gtk-application-argv` | `@gtkx/react`\'s `<GtkApplication>` bootstrap still builds the GApplication\'s own command line as `[applicationId.split(".").at(-1), ...process.argv.slice(2)]` and hands it to `runApplication(application, commandLine)` — confirmed unchanged by reading the installed 1.2.2 source (`components/application.tsx`) directly, no `argv` prop or options object anywhere on `ApplicationComponentProps`. Our runner hosts are invoked as `node dist/runner/host.js <bundle-path>` / `node dist/runner/host-dev.js <bundle-url>`, so `process.argv[2]` is our OWN internal plumbing, not a user-facing argument — left in place it reaches GLib\'s local command-line handling as a stray positional it tries to open as a file (`GLib-GIO-CRITICAL: This application can not open files`), and the window never activates | `process.argv.length = 2` right after each host reads its own positional, before the bundle runs (`src/runner/host.ts`, `host-dev.ts`) — both mount `<GtkApplication>` from a `useLayoutEffect`, which always fires after this line. Not needed on the vite path (`gtkx build`/`gtkx dev` bundles run with no extra positional) or the SEA build (which never reads `process.argv`) | `@gtkx/react` exposes a way to pass `runApplication`\'s argv explicitly (an `argv` prop on `<GtkApplication>`, or an options object) instead of always reading `process.argv` itself |',
1392
1666
  },
1393
1667
  {
1394
- doc: "docs/api.md",
1395
- heading: "DevSettings",
1396
- text: "DevSettingsSupported: `addMenuItem(title, handler)` (entries in the Dev MenuCtrl+Shift+D in `run-linux --dev`, the react-native-windows shortcut), `reload(reason?)`. Differences: silent no-ops in release builds, like RN",
1668
+ doc: "docs/gtkx-1.2-notes.md",
1669
+ heading: "How each was checked against 1.2.2",
1670
+ text: "The rule is that a changelog entry (or, for a stable release, a release page)\nis a claim and the removal condition is the test, so each row was re-run on\nthe real runtime rather than read about.\n\n- **`runtime-dedupe`** reproduced the exact shape the condition names —\n two DISTINCT `.node` files in one process — by copying the installed\n `@gtkx/native` + `@gtkx/native-linux-arm64-gnu` package trees (1.2.2) to\n two separate absolute paths, each under its own `node_modules/@gtkx/`\n (Node caches a native addon by the resolved path of the `.node` FILE, so\n two copies at different paths are two distinct files even with identical\n bytes) and importing both `main.js` entry points in one script. First\n import succeeds; the second raises `gtkx: GLib-ERROR:\ng_log_set_writer_func() called multiple times` as an uncaught Node\n exception, exit code 1, no core file reproduced twice for consistency.\n This is the SAME defect as 1.0 (`log_writer.rs`'s `install()` still uses a\n `OnceLock` — a WITHIN-one-binary guarantee only — confirmed unchanged\n since rc.4 by diffing the file across GitHub tags) but a DIFFERENT\n failure mode: 1.0's identical probe produced a bare SIGABRT/core dump\n (`exit 134`). The change traces to `node_env.rs`'s `raise_fatal`, which\n gained an explicit `napi_open_handle_scope`/`napi_close_handle_scope`\n pair around the raw `napi_fatal_exception` call in the 1.x line (absent\n at 1.0.0, confirmed by diffing `node_env.rs` at both tags) — see\n upstream-gtkx.md ask #2 for the full mechanism. Neither half of the\n removal condition is met: the error still does not name the duplicate\n package.\n- **`prop-portal`** — enumerated the real module exports by reading the\n compiled `dist/index.js` and `dist/internal.js` directly (same method as\n before): `@gtkx/react`'s `dist/index.js` exports exactly `useApplication`,\n `useBindSetting`, `useParentWindow`, `useProperty`, `useSetting`,\n `useSignal`, `rootElement`, `createPortal`, `createRoot`, `quit`;\n `dist/internal.js` exports `createApplicationWindowComponent`,\n `createApplicationComponent`, `createElementComponent`,\n `createPortaledComponent`, `createWindowComponent`, `useLatestRef`,\n `useMergedRef`, `settleAccessible`, `isRootElement`,\n `createReconcilerRoot`, `setReconcilerErrorHandler`. No value in either is\n `\"gtkx:prop\"` — `components/element.js` itself still exports both `Prop`\n and `createElementComponent`, but `internal.js` re-exports only the\n latter. `createPortal`'s own `.d.ts` signature is still `(children,\ncontainer, key?)`, byte-identical to rc.3/rc.4/1.0. A deep import of\n `@gtkx/react/dist/components/element.js` still throws `Package subpath\n'./dist/components/element.js' is not defined by \"exports\"` — reproduced\n fresh against 1.2.2. Functional confirmation:\n `tests/gtk/components/app-registry.gtk.test.tsx` (exercises\n `WindowActions`/`WindowControllers` built on `createSlotPortal`) passes\n clean, 6/6, on 1.2.2.\n- **`renderhook-no-window`** — reproduced the rc.4-era check verbatim, fresh\n on 1.2.2: in one throwaway test file, `Gtk.Window.getToplevels().getNItems()`\n before and after a bare `renderHook(() => null)` reads 0 → 0, then\n `render(null)` in the same file reads 0 → 1. `RenderHookOptions` in\n `dist/render-hook.d.ts` still carries only `wrapper` and `initialProps`,\n and `render-hook.tsx` still constructs `new Gtk.Box()` unconditionally as\n its container. Functional confirmation: `tests/gtk/apis/dimensions.test.tsx`\n (3/3) still passes using the `render()`-first pattern.\n- **`gtk-application-argv`** read `@gtkx/react@1.2.2`'s installed\n `components/application.tsx` source directly: `commandLine` is still\n `[applicationId?.split(\".\").at(-1) ?? \"gtkx\", ...process.argv.slice(2)]`,\n and `ApplicationComponentProps` still only carries `applicationId`,\n `children` and `ref` — no `argv` field, no options object, grepped the\n whole `@gtkx/react` and `@gtkx/runtime` trees for `argv` and found nothing\n else relevant. Functional confirmation: this task's own\n `npm run typecheck`/`build:dist`/`gallery-smoke.ts` runs on the merged\n worktree state (which mounts `<GtkApplication>` through the workaround)\n stay green.",
1397
1671
  },
1398
1672
  {
1399
- doc: "docs/api.md",
1400
- heading: "I18nManager",
1401
- text: "I18nManagerSupported: `isRTL` (live: GTK's read of the locale text direction), `doLeftAndRightSwapInRTL`, `getConstants`. Differences: `allowRTL`/`forceRTL`/`swapLeftAndRightInRTL` are accepted no-ops (mobile persistence has no desktop store)",
1673
+ doc: "docs/gtkx-1.2-notes.md",
1674
+ heading: "What 1.2 changed under us",
1675
+ text: "Unlike 1.0, 1.2 introduced **no new workaround**: the two breaks task 001\n(\"first light\") absorbed strict signal typing, the `bundle.mjs` rename —\nwere both plain fixes to our own code (a looser-than-actual type signature,\na hardcoded output filename), not gtkx behavior we have to route around, so\nneither has a row here or a removal condition to track.\n\n**1.2.1's criticals-raise-uncaught-exceptions change** (task 001's own\n\"criticals-throw\" probes already confirmed clean at the application level —\nscroll adjustment lore, modal open/close cycles, teardown) turned out to\nmatter for this task too: it is the same change that altered\n`runtime-dedupe`'s failure mode (see the row above and upstream-gtkx.md\nask #2), and re-probing it with two FRESH, more aggressive stress tests\n(a synchronous 5,000-call burst of invalid `Gtk.Adjustment.configure()`\ncalls with an `uncaughtException` handler installed, and a real\n`GtkWidget.addTickCallback`-driven burst of 2,000 identical calls under the\nactual `@gtkx/vitest` forks pool — both well past the 1,699-call burst that\noriginally triggered ask #2's crash) produced zero crashes on 1.2.2, where\nthe same infrastructure crashed on 2026-08-04. See upstream-gtkx.md ask #2\nfor the full writeup and the mechanism found.\n\n`registerClass` subclassing's chain-up capability (1.0's candidate answer to\nupstream-gtkx.md ask #3 — every generated `vfunc*` method `protected` and\nchainable via `super.vfuncX()`) is unchanged on 1.2.2, confirmed by reading\nthe installed `@gtkx/gi/gtk` typings directly; still not adopted into\n`layout-manager.ts`, per this epic's scope (adopting it is a follow-up, not\na migration task).\n\n`createDialogComponent` (upstream ask #10) is unchanged on 1.2.2: the\ninstalled `dist/adw/dialog.d.ts` still types its returned component as\n`(props: PresentedProps<Adw.Dialog>) => ReactNode` with `Adw.Dialog`\nresolved against `@gtkx/react`'s own store — not generic, not\nstore-agnostic. See \"Resolved by configuration\" below; nothing to revert.",
1402
1676
  },
1403
1677
  {
1404
- doc: "docs/api.md",
1405
- heading: "BackHandler",
1406
- text: 'BackHandlerSupported: `addEventListener("hardwareBackPress")`, `exitApp`. Differences: no hardware back key on desktopsubscriptions are honored but nothing fires them yet',
1678
+ doc: "docs/gtkx-1.2-notes.md",
1679
+ heading: "Resolved by configuration",
1680
+ text: "- `dialog-component-ref-widen` — @gtkx/react's `createDialogComponent` pins\n one store's `Adw.Dialog` in its returned props type, which breaks codegen\n typechecking for every workspace app with its own store (upstream ask #10\n in docs/upstream-gtkx.md, **still open on 1.2.2** the installed\n `dist/adw/dialog.d.ts` is unchanged, confirmed by reading it directly). We\n no longer carry a workaround: the examples set `codegen: false` and share\n the root-generated store — the sanctioned workspace shape\n (`removeSharedStoreShadow` in @gtkx/cli exists exactly for it), which\n makes the app and @gtkx/react resolve the same store by construction.\n This shape stays required, not optional, on 1.2.2the factory has not\n become generic. A patch-package patch briefly filled this slot during the\n 1.0 migration and was removed the same day: the installed `.d.ts` proved\n platform-specific for one published version, making any single patch\n unappliable on the other OS.",
1407
1681
  },
1408
1682
  {
1409
- doc: "docs/api.md",
1410
- heading: "findNodeHandle",
1411
- text: "findNodeHandleSupported: a stable integer per mounted widget, resolvable back to it; accepted by `measureLayout` as its first argument, alongside a handle object. Takes what RN takes a component handle, a node handle (returned unchanged), `null`/`undefined`. A windowed list resolves to the `ScrollView` it renders, as RN's `FlatList` resolves through to its own scroll view. Differences: The tag identifies the WIDGET, not the ref: two refs onto one view report the same number and a re-render that rebuilt the handle object does not change it. It reaches nothing nativethere is no `UIManager` to hand a tag to so it is worth exactly what this platform can resolve it to: `measureLayout`, and identity. `null` for anything that is not a mounted host view (RN's answer too)",
1683
+ doc: "docs/gtkx-1.2-notes.md",
1684
+ heading: "Fixed in 1.2.1 (history)",
1685
+ text: "- **`use-signal-stale-handler`** gtkx's oldest workaround, alive since\n rc.3. `useSignal` routed the handler through React's `useEffectEvent`,\n and `react-reconciler@0.33.0` only refreshed it in\n `commitBeforeMutationEffects` for `case 0` (FunctionComponent) —\n `case 11` (ForwardRef) and `case 15` (SimpleMemoComponent) fell through\n unrefreshed, so any `useEffectEvent` inside a `memo`/`forwardRef`\n component was pinned to its mount closure forever (our `ScrollView` is a\n `forwardRef` with the `useSignal` calls inside it — a fetch-fed FlatList\n emptied itself on the first scroll). Upstream had ruled on this before\n (gtkx-org/gtkx#467, our own fix offered as #469 and **closed unmerged on\n purpose** — @eugeniodepalo: \"closing this in favour of waiting for\n upstream… Since React fixes this properly on 19.3 for all fiber tags, I'd\n rather take the version bump than carry a workaround I'd revert\") — and\n it was re-checked and reconfirmed unfixed on both rc.4 and 1.0.0, since\n the removal condition names a React version a release could satisfy by\n bumping.\n **gtkx fixed it directly instead, without waiting for React 19.3.** The\n 1.2.1 changelog's own bugfix entry: \"Fixed `useSignal` running the handler\n captured on the first render for every emission inside a component\n wrapped in `memo` or `forwardRef`... The hook built on React 19.2's\n `useEffectEvent`, which does not pick up the updated function through\n those wrappers; the handler is now held in a ref written from\n `useInsertionEffect`.\" Confirmed by reading the installed 1.2.2 source\n directly: `@gtkx/react`'s `hooks/use-signal.ts` no longer imports\n `useEffectEvent` at all — it calls the package's own new `useLatestRef`\n (`hooks/use-latest-ref.ts`), a `useRef` refreshed by a `useInsertionEffect`,\n exactly the pattern our bridge wrapper used to restore by hand.\n `react-reconciler` stayed at `^0.33.0` and `react` stayed on `^19.2` in\n the installed 1.2.2 tree this is gtkx's own fix, not a React bump.\n Probed by flipping `tests/gtk/bridge/use-signal-upstream.gtk.test.tsx`'s\n `it.fails` guard into a plain `it` and running it targeted on 1.2.2: it\n now passes (previously, running the unflipped `it.fails` guard on 1.2.2\n first, per the \"reproduce the original failure first\" discipline, reports\n `Error: Expect test to fail` the wrapped assertion itself succeeds,\n meaning `it.fails`'s own expectation of failure is what fails). Deleted\n the workaround: `src/gtkx/bridge/use-signal.ts` (the ref-plus-\n `useInsertionEffect` wrapper) is gone, and `src/gtkx/bridge/core.ts`\n re-exports `useSignal` straight from `@gtkx/react` again, alongside the\n package's other hooks.",
1412
1686
  },
1413
1687
  {
1414
- doc: "docs/api.md",
1415
- heading: "Keyboard",
1416
- text: "KeyboardSupported: `addListener` (honoured, never fires), `removeAllListeners`, `dismiss`, `isVisible` (always false), `metrics` (always undefined), `scheduleLayoutAnimation`. Differences: This is the SOFTWARE keyboard, and a desktop has none: every event it carries describes a panel occluding the app, so none can fire. Subscriptions are real and `remove()` pairs with them (a fake subscription would turn an unmount into a crash) — the same shape as `BackHandler` above, and what react-native-windows inherits from RN core, whose emitter is only ever fed on iOS and Android. **`dismiss()` is a no-op and deliberately not RN's**: RN blurs the focused input as the only way to retract the keyboard, and doing that here would let a library's gesture steal focus from a form",
1688
+ doc: "docs/gtkx-1.2-notes.md",
1689
+ heading: "Fixed in rc.3 (history, one line each)",
1690
+ text: '- **`gsk-colorstop-boxed-write`** constructing a `Gsk.ColorStop` threw in\n the native addon, so SVG gradients had zero constructible stops and painted\n nothing. **Fixed upstream by us** (gtkx-org/gtkx#473, closing #472): a\n record field write converts through `toNative` now.\n- **`graphene-rect-nested-boxed-props`** the same native bug reached through\n `new Graphene.Rect({ origin, size })`; same upstream fix, so `svg-node.ts`\n uses the plain constructor again.\n- **The codegen freshness lie** rc.2\'s `@gtkx/cli` could report "bindings up\n to date" over a store `npm install` had pruned. Fixed upstream in\n gtkx-org/gtkx#470 (also ours); separately `src/runner` calls the\n programmatic `@gtkx/codegen` API rather than the CLI.',
1417
1691
  },
1418
1692
  {
1419
- doc: "docs/api.md",
1420
- heading: "LogBox",
1421
- text: "LogBoxSupported: `ignoreLogs`, `ignoreAllLogs`, `install`, `uninstall` accepted and ignored. Differences: RN's LogBox is a full-screen dev OVERLAY, and `ignoreLogs` has never filtered the console it keeps a warning out of the yellow box. There is no overlay here, so the console output after the call is already the console output RN would have had, and nothing observable is lost. Called by `react-native-draggable-flatlist` on every `NestableDraggableFlatList` render, to silence a nesting warning this platform does not emit either",
1693
+ doc: "docs/gtkx-1.2-notes.md",
1694
+ heading: "Fixed in rc.2 (history, one line each)",
1695
+ text: "- **`vitest-compositor`** rc.1 defaulted the headless display to weston;\n rc.2's default IS sway, so `vitest.config.ts` calls the plugin with no\n arguments.\n- **`no-virtual-seat`** rc.1 had no input seat under sway, so windows never\n activated and `userEvent` was impossible; rc.2 starts a virtual seat.\n- **`fixed-layout-child`** rc.1's declarative `<GtkFixedLayoutChild>`\n created a detached object; moot since containers moved to our own\n `RnGtkxLayout` manager and GtkFixed left the codebase.\n- **`controllers-as-children`** rc.1 silently ignored controllers passed as\n JSX; rc.2 has a `controllers` slot on `GtkWidget`. Pressable and TextInput\n still attach theirs imperatively on purpose a choice now, not a\n workaround.",
1422
1696
  },
1423
1697
  {
1424
- doc: "docs/api.md",
1425
- heading: "PanResponder",
1426
- text: "PanResponder Supported: `create(config)` -> `panHandlers` (spread onto a `View`), full `gestureState` (`dx`/`dy`, `vx`/`vy`, `x0`/`y0`, `moveX`/`moveY`, `numberActiveTouches`) **react-native's own file, vendored unmodified** (MIT, `Libraries/Interaction/PanResponder.js`), running on our reproduction of RN's `touchHistory` store. Differences: multi-touch `gestureState` is single-touch here (one pointer), and `onShouldBlockNativeResponder`'s return value is not consumed yet. `onPanResponderTerminationRequest` is asked when an ancestor tries to take the gesture and when an enclosing `ScrollView` scrolls; every other termination is GTK's decision and arrives as `onPanResponderTerminate` unasked (see `View`)",
1698
+ doc: "docs/gtkx-1.2-notes.md",
1699
+ heading: "Status of this migration",
1700
+ text: "The full suite (186+ files) has not run yet that is deliberately deferred\nto the `gtkx-1-2-migration` epic's task 003, which runs it exactly once for\nthe whole migration. What is confirmed on 1.2.2 as of this task: `npm run\ntypecheck` and `npm run build:dist` green (both before and after deleting\n`use-signal-stale-handler`), the flipped `use-signal-upstream.gtk.test.tsx`\nregression test green on a targeted run, and every probe in the tables above\n(four re-audited workarounds, two fresh upstream-ask #2 stress probes, one\nretired workaround) behaves as expected. Nothing here should be read as \"the\nsuite passes\" — that claim belongs to task 003 alone.\n\nTwo environmental notes carried forward from the rc.3/rc.4/1.0 era, neither\ngtkx-version-specific and both unexamined by this task:\n\n- **A first codegen after a version bump is slow enough to look like a hang.**\n The store fingerprint includes the app's own config, so each example\n regenerates once on top of the root's run ~45 s for the gallery on the\n VM. `scripts/gtkx-dev-headless.ts` sleeps 25 s before its first shot and\n will report `FAST-REFRESH-FAIL` on a cold store; run `npx gtkx codegen` in\n the example first.\n- **`gtkx dev` still binds vite's HMR websocket on the fixed port 24678**, and\n the CLI exposes no way to move it. A second `gtkx dev` anywhere on the\n machine logs `WebSocket server error: Port 24678 is already in use` and the\n edit never reaches the app, while the supervisor still prints \"Fast Refresh\n complete\" — so the log marker alone is not proof the refresh applied.",
1427
1701
  },
1428
1702
  {
1429
- doc: "docs/api.md",
1430
- heading: "Animated",
1431
- text: "Animated Supported: `Value`, `timing`, `spring`, `sequence`, `parallel`, `delay`, `loop`, `interpolate` (numbers and deg/rad strings, clamp/extend/identity), `ValueXY` (`setValue`/`setOffset`/`flattenOffset`/`extractOffset`, `getLayout`, `getTranslateTransform`) — the value a `PanResponder` drag writes to; `event(argMapping, config?)` the arg mapping is positional over the callback's own arguments (an array entry maps that argument, `null` skips it), traversed recursively into plain objects with a leaf that is a `Value`/`ValueXY` written on every call; works attached to `ScrollView`'s `onScroll` (mapping the one event argument) and to `PanResponder` callbacks such as `onPanResponderMove` (mapping `gestureState`, the second argument RN supports mapping either one); `config.listener` is called with the same arguments, after the mapping has run. Differences: `useNativeDriver` is ignored (with a warning); the direct path is native-speed anyway and because there is no native side to hand the event to, `Animated.event` always returns the plain JS handler, `useNativeDriver` true or false, rather than sometimes returning the opaque attach/detach object upstream does. A mapped path the real event does not carry is tolerated at any depth (silently left unset) rather than thrown, which is a deliberate widening of RN's own traversal — its direct property access throws one level above a missing leaf",
1703
+ doc: "docs/gtkx-1.2-notes.md",
1704
+ heading: "Behaviour rc.3 changed under us (still true)",
1705
+ text: "Carried forward from the rc.3/rc.4/1.0 notes; none of this is re-verified by\nthis task (it is `@gtkx/testing`-adjacent runtime behavior, out of this\nepic's scopesee the `gtkx-1-0-migration` epic's task 004 for the last\nfull re-check):\n\n- **Blockable signals are no longer suppressed for a whole React commit** —\n rc.3 wraps each framework write individually, so an emission the framework\n did not cause (one raised from a `useLayoutEffect`, or aimed at another\n `createRoot` tree) reaches its handler. Our navigators lean on this.\n- **`render`'s harness window is undecorated**, so role queries see only what\n the test rendered.\n- **A widget with `accessibleLabelledBy` reports the relation as its\n accessible name**, ahead of its own text the precedence ARIA defines.\n- **`toHaveTextContent` no longer falls back to the accessible name**;\n **`toHaveDisplayValue` throws** on a widget without one; **checked state is\n tri-state**.\n- **Records are constructible only when their bytes can be copied.** Neither\n `Gsk.ColorStop` nor `Graphene.Rect` is caught by it.\n- **Single-child widgets have no `content`/`child` props** (pass the widget as\n a child instead).",
1432
1706
  },
1433
1707
  {
1434
- doc: "docs/api.md",
1435
- heading: "Easing",
1436
- text: "Easing Supported: linear/ease/quad/cubic/in/out/inOut/bezier. Differences: —",
1708
+ doc: "docs/gtkx-1.2-notes.md",
1709
+ heading: "Non-workarounds (quirks that stay)",
1710
+ text: '- 64-bit FFI values arrive as BigInt → `toNumber()` at the boundary\n (`gtkx/bridge/measure.ts`);\n- signal names are kebab-case ("value-changed"); signals do not pass the\n emitter (get the widget from a ref);\n- role queries in tests use the `Gtk.AccessibleRole` enum, not strings;\n- `npm install` prunes the codegen store (`node_modules/.gtkx` is not in the\n lockfile) → run `npm run codegen` after installing — npm behavior, not gtkx;\n- measuring unmapped widgets yields 0 (offscreen Label probes are the\n exception) → re-measure on the `map` signal + re-commit measured leaves on\n every flush (`layout/node.ts`);\n- mixed-session setups only: running an app on a bare compositor (headless\n sway) while `XDG_RUNTIME_DIR` points at a full GNOME session can segfault in\n a GTK signal handler when the GNOME settings portal pushes updates into the\n app (`g_cclosure_marshal_VOID__OBJECTv` via the FFI emit path); cutting\n `DBUS_SESSION_BUS_ADDRESS` avoids it, which is why the headless scripts do.\n Normal desktop and container runs are unaffected. The portal-push crash\n needs a live settings change to trigger and stays on the list unconfirmed;\n- GLib criticals and errors now raise as a Node `uncaughtException` (1.2.1,\n see upstream-gtkx.md ask #2) a `g_return_if_fail` violation that used to\n scroll past in a log now ends an app with no `process.on("uncaughtException")`\n handler. `gtkx dev` already installs one; so does every headless probe\n script this repo uses.',
1437
1711
  },
1438
1712
  {
1439
- doc: "docs/api.md",
1440
- heading: "`requestAnimationFrame`/`cancelAnimationFrame`",
1441
- text: '`requestAnimationFrame`/`cancelAnimationFrame` Supported: **A global, not a module export** same as in RN itself, which installs both from its own bootstrap (`InitializeCore`) rather than exporting them from `"react-native"`. This platform dropped that bootstrap entirely (there is no mobile environment to polyfill, "the runtime IS Node"), which held for everything except this one browser-ism library code reaches for directly — `react-native-sortables` crashes the whole process at mount without it (docs/research/upstream-libraries.md, "A third experiment"). Installed as a side effect of the package entry (`src/index.ts`), the one module both toolchains load before any app code runs; built on `components/frame-scheduler.ts`\'s `glibScheduler` the same clock `Animated`/`Reanimated` already share, not a second timer. An id is returned; the callback receives a monotonic, high-resolution timestamp; a callback requested while a batch is running lands on the NEXT frame (never the one currently flushing); cancelling is silent, including for an unknown or already-delivered handle; one callback throwing is reported (`console.error`) and does not stop its siblings in the same batch. Differences: parity in behavior; not parity in mechanism there is no native frame on a Linux desktop the way there is on iOS/Android, so this rides the same ~60 fps GLib timer `Animated` does, the way the DOM\'s `requestAnimationFrame` stands in for it on react-native-web',
1713
+ doc: "docs/gtkx-1.2-notes.md",
1714
+ heading: "Procedure when the next release ships",
1715
+ text: '1. Update the `@gtkx/*` pins (root, spike, examples, template), then\n `npm install && rm -rf node_modules/.gtkx && npm run codegen`;\n2. Run everything on Linux: `npm run typecheck && npm test`, `build:dist`,\n `check:package`, plus the headless example proofs;\n3. Walk the live-workaround table: for each row check the removal condition,\n delete the tag and the row together when it is met, and move the entry into\n the history section above **with a probe that proves the fix on the real\n runtime**, not just the release notes claiming it. And make the probe\n reproduce the ORIGINAL failure first: two of the three `runtime-dedupe`\n probes written for rc.4 reported a fix that was not there, because they\n were not actually building the duplicate; the `use-signal-stale-handler`\n guard the 1.2 audit flipped is the same discipline the other direction —\n running the UNFLIPPED `it.fails` guard on the new runtime first, so the\n "now fails to fail" observation is the actual probe, not an assumption.\n The same discipline applies to every future audit, this one included;\n4. Re-tag whatever survives (`1.2-WORKAROUND` the new release), rename this\n file to match the new pin, and update `docs/upstream-gtkx.md` if an ask was\n answered.',
1442
1716
  },
1443
1717
  {
1444
- doc: "docs/api.md",
1445
- heading: "version",
1446
- text: "version Supported: package version. Differences: extension",
1718
+ doc: "docs/research/navigation-extensibility.md",
1719
+ heading: "1. The two layers",
1720
+ text: "```\nyour app\n ├── react-native portable components\n ├── react-native-gtkx/navigation react-navigation adapter (optional)\n └── react-native-gtkx/adwaita GTK widgets and primitives\n```\n\n**`react-native-gtkx/gtk` and `react-native-gtkx/adw`** owns the widget: diffing a requested stack\nof tags into `pushByTag` / `popToTag` / `replaceWithTags`, holding a popped\npage alive until its exit animation ends, bracketing transitions, reporting\nnative pops. It imports nothing from `@react-navigation/*`. `NavigationStack`\ntakes the visible stack as a prop, so a `useState` is a complete router.\n\n**`react-native-gtkx/navigation`** is an adapter: react-navigation state to\nan array of tags, a native pop to `StackActions.pop` (only when the tag is\nstill in state, otherwise it would double-pop), descriptors to titles, header\ncontent and `canPop`, plus dev warnings for options we ignore.\n\nThis is the same split the React Native ecosystem already uses:\n`react-native-screens` exposes primitives, `@react-navigation/native-stack`\nbinds them to a router. It is also what React Navigation's maintainer\nrecommended when he saw the project (u/satya164, on the r/reactnative\nannouncement): _keep your own navigator so you can provide options specific\nto GTK, unless you plan to match native stack API 1:1._\n\nThe consequence that matters: **the ceiling of react-navigation's model is\nnow only in the adapter, never in the primitive.** A GTK capability with no\ncounterpart in React Native does not have to be squeezed into someone\nelse's abstraction — it lives in the primitive layer and is reachable\ndirectly. See [../architecture/overview.md](../architecture/overview.md).",
1721
+ },
1722
+ {
1723
+ doc: "docs/research/navigation-extensibility.md",
1724
+ heading: "2. What an app can reach today",
1725
+ text: "Everything below the HeaderBar: each page hosts a full RN tree in its own\nlayout root. All of react-navigation's state mechanics: params,\n`setOptions`, dispatch, resets.\n\nStack options: `title`, `headerShown`, `headerButtons` (declarative native\nicon buttons), `headerLeft` / `headerRight` (ordinary RN content rendered\n_inside_ the HeaderBar), `gestureEnabled`.\n\nPast the options, the primitives: any GTK widget we bind, taking `style` so\nReact Native drives its position and its appearance, plus `wrapReactNative`\nfor widgets we do not re-export, plus a `ref` to the underlying\n`Adw.NavigationView`. There is no wall — a missing convenience costs a line,\nnot a fork.\n\n**Resolved since the first snapshot.** Kept here because the reasons are\nstill instructive:\n\n- _RN content could not size a chrome slot_ (HeaderBar start/end, sidebar\n rows) — one root cause behind the whole `headerLeft`/`headerRight` class.\n Fixed by the intrinsic-size root, now public as `IntrinsicContent`.\n- _`usePreventRemove` / `beforeRemove` desynced_, because the native pop had\n already happened when state heard about it. Fixed through\n `AdwNavigationPage:can-pop`: a prevented route cannot be popped by the\n user at all, so there is nothing to race. Covered by\n `tests/gtk/navigation/prevent-remove.gtk.test.tsx`.\n- _Unsupported options were ignored silently._ Fixed:\n `src/navigation/option-warnings.ts` names the screen and the option in\n development.\n- _Screen props and options had to be hand-rolled._ Fixed:\n `createStackNavigator<ParamList>()` types `Stack.Screen`, its options and\n the screen props (`examples/hn-app` relies on it).\n\n`createSidebarNavigator`'s own gaps — sidebar row rendering, collapsed\nmode and the static content header — are covered in §3 below, alongside\nthe `examples/tasks-app`/`examples/tasks-nav` narrative that found and\nthen closed them.\n\nOn typing, one clarification worth recording, since it was raised publicly.\nThe complaint was never that custom navigators cannot be typed — the docs\nshow how, and we follow them. It is that the upstream v7 signature is\n`createNavigatorFactory(Navigator: ComponentType<any>): (config?: any) => any`,\nso nothing flows out of the factory itself and the types have to come from\nannotating the navigator. React Navigation 8 replaces this with a real typed\nAPI (`NavigatorTypeBagBase`, `createScreenFactory`); adopting it is the\n`react-navigation-8` epic.",
1726
+ },
1727
+ {
1728
+ doc: "docs/research/navigation-extensibility.md",
1729
+ heading: "3. Still open",
1730
+ text: "Meaningful on this platform and not done yet: toolbar top-bar style (the\n`headerTransparent`/`headerShadowVisible` analogue), search-bar options\n(`Gtk.SearchBar` / `headerSearchBarOptions` — note v8 renamed its\n`onChangeText` to `onChange`), and deep links (they parse, but nothing\ndelivers a URL on the desktop yet). `animation: \"none\"` is done (a screen\noption, see docs/api.md).\n\n**Resolved by building `examples/tasks-app` (the gtkx tutorial's Tasks app,\nported), each with a small library change, not a workaround:**\n\n- _`Adw.Dialog` presentation_ — confirmed working. `AdwAboutDialog`/\n `AdwAlertDialog`/`AdwPreferencesDialog`/`AdwShortcutsDialog` are already\n `wrapReactNative`-wrapped; mounted with no Yoga ancestor anywhere in the\n tree (this app has none — see the example's README), they hit\n `wrapReactNative`'s \"bare\" branch and present correctly, verified live\n with real screenshots (Preferences, Shortcuts). Nothing to fix here —\n this item can be dropped from \"still open\" entirely.\n- _Breakpoints_ — a real `Adw.Breakpoint`, verified live collapsing the\n window at a narrow width, but not through the navigator: through a new\n `AppRegistry.runApplication({ breakpoints })` parameter instead (the\n navigator itself still had no collapsed-mode concept at the time —\n closed by `navigation-depth-2`, see below). Also found and recorded:\n `AdwBreakpoint`'s `onApply`/`onUnapply` never fire under the\n `@gtkx/vitest` headless-sway gtk test project, in any form tried (JSX\n prop, imperative `Adw.Breakpoint`+`addBreakpoint`, a genuine `swaymsg`\n resize) — but fire immediately in a real GNOME session. Treat it as\n untestable headless today, not broken; see\n `packages/react-native-gtkx/tests/gtk/bridge/auxiliary-elements.gtk.test.tsx`.\n (`navigation-depth-2`'s own `collapseWidth`, below, sidesteps this\n entirely — it drives `Adw.Breakpoint.addSetter` rather than\n `onApply`/`onUnapply`, and that IS testable headless, see\n `tests/gtk/adw/breakpoint.gtk.test.tsx`.)\n- _Actions and menus_ were never on this list by name, but turned out to\n be the same kind of gap: `AppRegistry.runApplication` had no way to\n attach a `GSimpleAction`, `actionAccels` or a `GtkShortcutController` to\n the app/window it builds — required for a `Gio.Notification` action\n button to route anywhere at all. Closed the same way, with\n `applicationActions`/`actionAccels`/`windowActions`/`windowControllers`.\n\n**Resolved by building `examples/tasks-nav` (`navigation-depth-2` epic),\nclosing exactly what the tasks-app port above found still narrow:**\n\n- _Sidebar row rendering and collapsed mode_ — `createSidebarNavigator`'s\n `SidebarNavigationOptions` was `{ title }` only: no per-row icon/color/\n count, and no collapsed/breakpoint wiring of its own (tasks-app had to\n reach `AppRegistry`'s `breakpoints` directly and drive `collapsed`\n itself). Fixed: `icon`/`color`/`count` (rendered as `AdwActionRow`, the\n same widget tasks-app's own hand-rolled sidebar used) and an opt-in\n `collapseWidth` prop, driving collapse through the navigator itself via\n a native `Adw.Breakpoint` — not a `useWindowDimensions` conditional; see\n [../architecture/layout-and-styling.md](../architecture/layout-and-styling.md),\n \"Two ways to react to size\", for the mechanism and why no `useBreakpoint`\n hook exists.\n- _One static content header shared by the whole navigator_ — the same\n port's other finding: a filter toggle group vs. a back button,\n depending on selection, did not fit one static header. The\n `navigation-depth-2` PRD explicitly allowed this turning out to be a\n structural gap; it wasn't — descriptor options already merge\n navigator-level `screenOptions` with a screen's own `options` and\n re-resolve on `navigation.setOptions()`, core react-navigation behavior.\n `SidebarNavigationOptions` gained `headerLeft`/`headerRight`/\n `headerTitle`, mirroring the stack navigator's own `headerLeft`/\n `headerRight`; a screen that toggles local state and calls\n `setOptions` in an effect gets a header that changes shape with its own\n selection, no stack involved — confirming tasks-app's own conclusion\n that a stack was never the right tool for the \"open an item\" case.\n Caveat found while testing this: `setOptions` merges into the\n previously resolved options rather than replacing them (see\n docs/api.md).\n\n`examples/tasks-nav` is the same navigational shape as `examples/tasks-app`\n— smart views, colored user lists, an open-item editor — now written\nthrough `createSidebarNavigator` instead of directly on\n`AdwNavigationSplitView`/`AdwActionRow`.\n\n**Resolved by `collapse-nav` (a live bug report on `examples/tasks-nav`),\none property lower than `collapseWidth` itself:** `collapseWidth` flips\n`AdwNavigationSplitView.collapsed` correctly, but `showContent` — WHICH\npane is visible while collapsed — was only half-wired: a row click already\nrevealed content, but nothing observed the split view's own back\naffordance putting it back, and a plain programmatic `navigate()` (no row\nclick) did not reveal content at all. On read, this looked like it might\nbe the same \"the breakpoint effect sets only `collapsed`\" gap all over\nagain; it mostly was not — see `sidebar.tsx`'s own file header for what was\nalready there. Three questions were settled empirically, with a throwaway\nGTK test written BEFORE any implementation code, rather than assumed from\nlibadwaita's docs:\n\n- _Does a cold-started, already-collapsed window default to content or the\n sidebar?_ Sidebar — `showContent` defaults to `false`, confirmed by\n mounting a window already narrower than `collapseWidth` and reading the\n property on first layout, before any code (ours or the app's) ever wrote\n to it. No fix needed.\n- _Does resizing back above `collapseWidth` and back below it need to\n reset `showContent` or the selection?_ No — both persist across the\n round trip, confirmed the same way (resize wide, resize narrow again,\n read the property). This is deliberate, not an oversight: it is the same\n size-class persistence a mobile master-detail app relies on (open an\n item, rotate to landscape and back, still on that item), which is\n exactly the \"the way a mobile app does\" behavior the bug report asked\n for. Resetting it would have fought the platform's own default for no\n benefit.\n- _Does an app need to observe or control the collapsed pane at all?_ One\n direction, yes: going back. TabRouter's `state` never changes when the\n user backs out of collapsed content (nothing is removed, the same route\n stays focused), so there is no existing react-navigation mechanism for\n an app to notice it happened — unlike a stack pop, which state itself\n already reveals through the route array shrinking. A new event,\n `sidebarShown` (`SidebarNavigationEventMap`, the same `navigation.emit`/\n `addListener` protocol `StackNavigationEventMap`'s `transitionStart`/\n `transitionEnd` already established — not a second protocol), fires on\n the active route for exactly this. The forward direction (content being\n revealed) got no event: it is already an ordinary state change an app\n can observe the normal way, so an event there would be pure duplication.\n\nThe echo risk this raises — state → widget and widget → state both touch\nthe same property, could they retrigger each other? — resolved the same\nway the stack navigator's own doc warns about it: by a value asymmetry, not\na flag. State → widget only ever WRITES `true`; widget → state only ever\nREACTS to `false`. Two disjoint values, so neither side can mistake the\nother's write for the other direction.\n\nFixed: `sidebar.tsx`'s `state.index` effect now also calls\n`showContentIfCollapsed()` (previously only `onRowActivated` did, so a\nclick worked but a programmatic navigation left the user stranded on the\nsidebar exactly like the report — a real, reproducible gap, not merely a\ntheoretical one); `onNotifyShowContent` is observed and re-emitted as\n`sidebarShown`. `examples/gallery` (no `collapseWidth`) is untouched by\nconstruction — every changed path checks `getCollapsed()` /\n`collapseWidth !== undefined` live first. See\n`tests/gtk/navigation/sidebar-collapse.gtk.test.tsx` for the automated\nversion of all four findings above, and docs/api.md for the public shape.\n\n**Found while building `examples/tasks-nav`, narrower, still open:**\n\n- _The sidebar PANE's own chrome has no customization hook_ — its\n `AdwToolbarView`'s `AdwHeaderBar` is hard-coded\n (`src/navigation/sidebar.tsx`); a navigator consumer can set\n `sidebarTitle` (a string) on it and nothing else. `examples/tasks-nav`'s\n \"New List\" action wanted to live there (matching tasks-app's own\n `SidebarHeader` component) but had to go on the content header instead,\n via the navigator-level `headerButtons` prop. Not on the PRD's\n checklist, so not built.\n- _Toasts_ — no `AdwToastOverlay`/`Adw.Toast` convenience exists anywhere\n in `react-native-gtkx` (upstream's own tutorial reaches for\n `@gtkx/components/adw`'s `ToastProvider`/`useToast`, a package this repo\n does not depend on). `examples/tasks-app/src/toast.tsx` is a local\n stand-in; the toast's underlying state change works and is verified live,\n but the toast's own visual appearance could not be confirmed on screen\n in that session, for a reason not yet root-caused. Worth a real fix (or\n at least a live confirmation) before another app leans on it.\n\n**Meaningless on desktop, skip forever:** status-bar and home-indicator\noptions, large titles, blur effects, gesture direction, form sheets,\nback-button labels. `headerBackButtonMenuEnabled` is free — libadwaita's\nback button already shows a history menu.",
1731
+ },
1732
+ {
1733
+ doc: "docs/research/navigation-extensibility.md",
1734
+ heading: "4. Porting an existing react-navigation app",
1735
+ text: "Compatible by construction: a real `@react-navigation/native` v8 peer, the\nofficial `useNavigationBuilder` and routers, a real `NavigationContainer`.\nEverything from react-navigation is imported from `@react-navigation/native`\ndirectly. We used to re-export a partial set from our navigation entry point\nand dropped it: the set was incomplete, so consumers ended up importing from\nboth places and could not tell which symbol came from where.\n\nMandatory changes: swap `createNativeStackNavigator` for our\n`createStackNavigator`; drop `react-native-screens`,\n`react-native-safe-area-context` and `react-native-gesture-handler` (all\nthree are mobile-native dependencies with nothing to bind to here).\n\nKeeping shared code portable: Linux-only options go behind a `.linux.tsx`\nplatform extension or `Platform.select({ linux: … })`. Options a platform\ndoes not understand are ignored — and here, warned about in development.",
1736
+ },
1737
+ {
1738
+ doc: "docs/research/navigation-extensibility.md",
1739
+ heading: "5. The desktop-RN landscape",
1740
+ text: "No other desktop React Native platform has native navigation integration:\n\n- **react-native-screens** lists Windows support, but it is a thin\n old-architecture module; native-stack on modern react-native-windows\n fails, because screens has no new-architecture Windows implementation and\n RNW 0.82 removed the old one. Microsoft's own react-native-gallery falls\n back to the JS drawer.\n- **react-native-macos** is not supported by react-native-screens at all —\n AppKit has no navigation-stack primitive to bind to.\n\nOur path — a real `Adw.NavigationView` driven from a custom navigator, with\nreact-navigation state as the source of truth and native pops reported back\ninto it — is structurally the iOS native-stack / `UINavigationController`\nmodel, which neither desktop platform reached. GTK's advantage is that the\nprimitive exists at all: back button, Escape, back gesture, history menu and\ntransitions ship with the widget.\n\nThe trade: a JS stack can render anything into its fake header, while our\nchrome is real and had to be opened up deliberately — which is what the\nintrinsic-size root does.\n\nSources: react-native-screens README and discussions #1575 / #2541, RNW\ndiscussions #14273 / issue #4152 / new-architecture docs / 0.82 release\npost, microsoft/react-native-gallery, reactnavigation.org native-stack docs.",
1741
+ },
1742
+ {
1743
+ doc: "docs/gtkx-1.2-notes.md",
1744
+ heading: "1.2-WORKAROUND(runtime-dedupe)",
1745
+ text: "Two bundled copies of the gtkx runtime still double-init GLib and abort; nothing guards against it across two DISTINCT `.node` files. Reproduced on 1.2.2 by actually building the duplicate: `gtkx: GLib-ERROR: g_log_set_writer_func() called multiple times`. The FAILURE MODE changed since 1.0 (see upstream-gtkx.md ask #2) — it is now a catchable, named Node `uncaughtException` instead of a SIGABRT/core dump — but the underlying defect has not: the init is still not idempotent across distinct binaries, and the error still does not name the duplicate package — our workaround: `src/vite/index.ts` puts `resolve.dedupe` over `@gtkx/*` + `react` (+ `@react-navigation/*` for its context) into the preset every app inherits — removed when: Idempotent runtime init upstream across distinct native-addon files, or an error that names the duplicate",
1746
+ },
1747
+ {
1748
+ doc: "docs/gtkx-1.2-notes.md",
1749
+ heading: "1.2-WORKAROUND(prop-portal)",
1750
+ text: '`createPortal(children, container, key?)` is still byte-for-byte the rc.3/rc.4 signature and can still only target a container\'s DEFAULT slot ("children"). `@gtkx/react`\'s public entry and its `/internal` subpath both still withhold the `"gtkx:prop"` element — `/internal` still re-exports `createElementComponent` (the function that BUILDS an intrinsic element by GType name) but not the `Prop` literal itself, which stays a sibling export inside `element.js` that no public or `/internal` path reaches — our workaround: `gtkx/bridge/slot-portal.ts` restates the `"gtkx:prop"` element name and wraps it in `createSlotPortal(children, target, slot)` — the one line that would move if gtkx renames it. `WindowActions`/`ApplicationActions`/`WindowControllers` are built on it — removed when: gtkx exports a slot-aware portal (or the prop element) from a public entry point',
1751
+ },
1752
+ {
1753
+ doc: "docs/gtkx-1.2-notes.md",
1754
+ heading: "1.2-WORKAROUND(renderhook-no-window)",
1755
+ text: "`renderHook` still mounts into a bare `Gtk.Box` — 1.2.2's `render-hook.js` still passes `container: new Gtk.Box()` unconditionally, and `RenderHookOptions` still carries only `wrapper` and `initialProps` — so window-dependent APIs have no toplevel to read — our workaround: Hook tests create a window with `render()` first (`tests/gtk/apis/dimensions.test.tsx`); packaged for consumers as `renderHookWithWindow` (`react-native-gtkx/testing`) — removed when: `renderHook` mounts into the same harness window `render` uses",
1756
+ },
1757
+ {
1758
+ doc: "docs/gtkx-1.2-notes.md",
1759
+ heading: "1.2-WORKAROUND(gtk-application-argv)",
1760
+ text: "`@gtkx/react`'s `<GtkApplication>` bootstrap still builds the GApplication's own command line as `[applicationId.split(\".\").at(-1), ...process.argv.slice(2)]` and hands it to `runApplication(application, commandLine)` — confirmed unchanged by reading the installed 1.2.2 source (`components/application.tsx`) directly, no `argv` prop or options object anywhere on `ApplicationComponentProps`. Our runner hosts are invoked as `node dist/runner/host.js <bundle-path>` / `node dist/runner/host-dev.js <bundle-url>`, so `process.argv[2]` is our OWN internal plumbing, not a user-facing argument — left in place it reaches GLib's local command-line handling as a stray positional it tries to open as a file (`GLib-GIO-CRITICAL: This application can not open files`), and the window never activates — our workaround: `process.argv.length = 2` right after each host reads its own positional, before the bundle runs (`src/runner/host.ts`, `host-dev.ts`) — both mount `<GtkApplication>` from a `useLayoutEffect`, which always fires after this line. Not needed on the vite path (`gtkx build`/`gtkx dev` bundles run with no extra positional) or the SEA build (which never reads `process.argv`) — removed when: `@gtkx/react` exposes a way to pass `runApplication`'s argv explicitly (an `argv` prop on `<GtkApplication>`, or an options object) instead of always reading `process.argv` itself",
1761
+ },
1762
+ {
1763
+ doc: "docs/reference/components/view.md",
1764
+ heading: "View",
1765
+ text: "View — GTK implementation: `GtkBox` (a custom paintable box). Supported: `style`, `onLayout`, `testID`, children. `pointerEvents` — `auto` / `none` / `box-none` / `box-only`, mapped onto GTK picking (a can-target flag plus a `contains()` override). Also honored from `style.pointerEvents`, with the prop taking precedence. `focusable` plus `onFocus` / `onBlur` — off by default, as in RN. A ref exposing `measure` / `measureInWindow` / `measureLayout` (`ViewHandle`, RN's own argument order — window coordinates come from `gtk_widget_compute_point`, so they read correctly inside a scrolled viewport). The full responder and touch prop set — `onStartShouldSetResponder(Capture)`, `onMoveShouldSetResponder(Capture)`, `onResponderGrant/Start/Move/End/Release/Terminate`, `onTouchStart/Move/End/Cancel` plus `Capture`; `PanResponder`'s `panHandlers` spread here too. See [Gestures](../../architecture/gestures.md).. Differences from RN: Input is single-pointer: a mouse is one fabricated touch, and `touches` never exceeds one. Responder negotiation is RN's model in full — capture-then-bubble, mid-gesture transfer through `onResponderTerminationRequest` / `onResponderReject`, one lock per process — but the negotiation path stops at the layout root, so native GTK widgets between or above views take no part in it. GTK settles most terminations before JS is consulted: a context menu, a native widget or `GtkDragSource` taking the sequence, and text selection all arrive as an already-cancelled gesture and terminate **without** consulting `onResponderTerminationRequest` — GTK's claim is irrevocable. Window blur terminates unconditionally (as on react-native-web). An enclosing `ScrollView` scrolling under the gesture is the one termination the responder may still refuse. `overflow: \"hidden\"` (and `\"scroll\"`, which clips identically) clips both the paint and the picking of children — including transformed ones and children an animation drives outside the box. `borderRadius` shapes that clip. A container never clips its own background, border, shadow or outline — only its children's.",
1766
+ },
1767
+ {
1768
+ doc: "docs/reference/components/text.md",
1769
+ heading: "Text",
1770
+ text: "Text — GTK implementation: `GtkLabel` (Pango). Supported: wrapping, `numberOfLines` (end ellipsis), `textAlign`, font styles, `onLayout`, `testID`, and a ref exposing the geometry methods (`TextHandle` — a label needs no wrapping `View` to be measurable).. Differences from RN: Nested `Text` elements are concatenated without per-span styling. Text is always ellipsizable — it shrinks in a narrow window rather than overflowing.",
1771
+ },
1772
+ {
1773
+ doc: "docs/reference/components/image.md",
1774
+ heading: "Image",
1775
+ text: "Image — GTK implementation: `GtkPicture`. Supported: `source={{ uri }}` or a string — local paths, `file://` and `http(s)` (fetched through Node and cached to disk by URL, with in-flight requests de-duplicated). `resizeMode` — `cover` / `contain` / `stretch` / `center`. `onLoad` / `onError`; a ref exposing the geometry methods (`ImageHandle`). `.svg` files load like any other image (rasterized through librsvg). Building vector graphics from state instead of a file is a separate import — see [Svg](../svg.md).. Differences from RN: A remote image has no synchronous size — `style` sets the size, as in RN. The disk cache is not size-limited yet.",
1776
+ },
1777
+ {
1778
+ doc: "docs/reference/components/safe-area-view.md",
1779
+ heading: "SafeAreaView",
1780
+ text: "SafeAreaView — GTK implementation: `View`. Supported: —. Differences from RN: A desktop window has no notch to inset around, so this renders exactly as `View`, with no safe-area insets applied.",
1781
+ },
1782
+ {
1783
+ doc: "docs/reference/components/status-bar.md",
1784
+ heading: "StatusBar",
1785
+ text: "StatusBar — GTK implementation: renders nothing. Supported: —. Differences from RN: There is no status bar on a desktop window, so every prop is accepted and ignored.",
1786
+ },
1787
+ {
1788
+ doc: "docs/reference/components/activity-indicator.md",
1789
+ heading: "ActivityIndicator",
1790
+ text: "ActivityIndicator — GTK implementation: `GtkSpinner`. Supported: `animating`, `size` (`small` / `large` / a number).. Differences from RN: `color` is not supported yet.",
1791
+ },
1792
+ {
1793
+ doc: "docs/reference/components/root.md",
1794
+ heading: "Root",
1795
+ text: "Root — GTK implementation: an internal layout root. Supported: `width`, `height`.. Differences from RN: Extension: the root the test harness renders a tree into.",
1796
+ },
1797
+ {
1798
+ doc: "docs/reference/components/nested-root.md",
1799
+ heading: "NestedRoot",
1800
+ text: "NestedRoot — GTK implementation: an internal layout root. Supported: —. Differences from RN: Extension: a Yoga layout root inside any GTK container slot (a navigation page, a custom container) — the slot's own allocation is the viewport.",
1801
+ },
1802
+ {
1803
+ doc: "docs/reference/components/intrinsic-root.md",
1804
+ heading: "IntrinsicRoot",
1805
+ text: "IntrinsicRoot — GTK implementation: an internal layout root. Supported: —. Differences from RN: Extension: a content-sized Yoga root for chrome slots (a header bar's start/end content) — it reports its content size to GTK instead of receiving an allocation.",
1806
+ },
1807
+ {
1808
+ doc: "docs/reference/components/text-input.md",
1809
+ heading: "TextInput",
1810
+ text: "TextInput — GTK implementation: `GtkEntry` (single line) / `GtkTextView` (multiline). Supported: Controlled and uncontrolled use (`value` / `defaultValue`), `onChangeText`, `onSubmitEditing`, `onFocus` / `onBlur`. `placeholder` — its own dim overlay in multiline mode, since `GtkTextView` has none built in. `secureTextEntry`, `editable`, `keyboardType`, `multiline`. `clearButtonMode` — `GtkEntry`'s built-in clear icon (RN only ships this on iOS). The visual half of `style` — background, border and radius all reach the widget, rather than being computed and dropped.. Differences from RN: Multiline needs an explicit `height` in its style, exactly as RN recommends. A real `GtkTextView` wraps words, scrolls internally, and inserts a newline on Enter rather than firing `onSubmitEditing` — RN's own multiline semantics.",
1811
+ },
1812
+ {
1813
+ doc: "docs/reference/components/switch.md",
1814
+ heading: "Switch",
1815
+ text: "Switch — GTK implementation: `GtkSwitch`. Supported: `value` / `onValueChange`, `disabled`.. Differences from RN: Sized by the GTK theme, not by iOS metrics.",
1816
+ },
1817
+ {
1818
+ doc: "docs/reference/components/pressable.md",
1819
+ heading: "Pressable",
1820
+ text: "Pressable — GTK implementation: `View` + click/motion event controllers. Supported: `onPress(In/Out)`, `onLongPress` (`delayLongPress`), `onHoverIn` / `onHoverOut`, `onFocus` / `onBlur`, `focusable`, `disabled`. A function-form `style` / `children` receiving `{ pressed, hovered, focused }` (react-native-web's own state shape). Keyboard-operable: `focusable` defaults to `true` whenever `onPress` is set (react-native-web's rule), which puts the view in the GTK focus chain — Tab and the arrow keys reach it, and Enter/Space fire `onPress` as they do on web and Android. The `PressEvent` payload matches RN's shape (`locationX/Y` target-relative, `pageX/Y` window-relative, `identifier`, `target`, `force`, a monotonic `timestamp`, single-element `touches`/`changedTouches`). `hitSlop` and `pressRetentionOffset` each take a number or a per-edge object; the press rect defaults to RN's own `{ top: 20, left: 20, right: 20, bottom: 30 }` around the hit rect, and releasing outside it cancels rather than presses.. Differences from RN: `hitSlop` cannot escape a clipping ancestor — a `ScrollView` viewport or any view with `overflow: \"hidden\"` — because GTK stops hit-testing at the clip; RN documents the identical limit on Android for the same reason. Hover fires from touch input as well as from a mouse (react-native-web filters that out; here a crossing event carries no device to filter on) — GTK also sends a matching leave when a touch sequence ends, so the stuck phantom hover the filter guards against does not arise; GTK's own `:hover` behaves the same way.",
1821
+ },
1822
+ {
1823
+ doc: "docs/reference/components/touchable-opacity.md",
1824
+ heading: "TouchableOpacity",
1825
+ text: "TouchableOpacity — GTK implementation: built on `Pressable`. Supported: `activeOpacity`.. Differences from RN: —",
1826
+ },
1827
+ {
1828
+ doc: "docs/reference/components/touchable-highlight.md",
1829
+ heading: "TouchableHighlight",
1830
+ text: "TouchableHighlight — GTK implementation: built on `Pressable`. Supported: `underlayColor` (default `black`, as in RN), `activeOpacity`, `onShowUnderlay` / `onHideUnderlay`.. Differences from RN: RN renders a separate underlay view behind the child and dims the child onto it. Here the highlight is the view's own `backgroundColor` while pressed — an extra box would change flex layout and what `measureLayout` measures relative to, the same reason `GestureDetector` and `createAnimatedComponent` add none either. Give the child a translucent background for RN's exact blend.",
1831
+ },
1832
+ {
1833
+ doc: "docs/reference/components/touchable-without-feedback.md",
1834
+ heading: "TouchableWithoutFeedback",
1835
+ text: "TouchableWithoutFeedback — GTK implementation: built on `Pressable`. Supported: the same press/hover/focus props as `Pressable`, with no visual reaction.. Differences from RN: RN clones its single child rather than rendering a box of its own — its own documentation calls that a compatibility artifact. This renders the `Pressable` box instead. Prefer `Pressable` directly, as RN's own docs recommend.",
1836
+ },
1837
+ {
1838
+ doc: "docs/reference/components/scroll-view.md",
1839
+ heading: "ScrollView",
1840
+ text: "ScrollView — GTK implementation: `GtkScrolledWindow`. Supported: Vertical and `horizontal` scrolling. `contentContainerStyle` — the content container is a plain `View`, so `alignItems` defaults to `stretch` as it does in RN. `onScroll` (`contentOffset`, `contentSize`, `layoutMeasurement`), the four scroll-phase callbacks `onScrollBeginDrag`/`onScrollEndDrag`/ `onMomentumScrollBegin`/`onMomentumScrollEnd`, `onContentSizeChange`. `stickyHeaderIndices` — the real child is translated and painted on top, no duplicate node. A ref exposing `scrollTo`/`scrollToEnd` plus the geometry methods `measure`/`measureInWindow`/`measureLayout` (`ScrollViewHandle`). A child that takes the responder suspends the scroller's own gestures for the rest of the interaction, so a pan gesture is reachable inside a scrolling list.. Differences from RN: `animated` in `scrollTo` is ignored. **The scroll phases are input-device aware**: a mouse wheel gives GTK isolated detents, so a burst is grouped into one begin/end session (a 120&nbsp;ms idle boundary) and reports no momentum; a touchpad glide reports all four phases from its native GTK sequence, and content really keeps moving once the fingers lift. RN has no wheel input, so the wheel session is a desktop-only extension rather than a parity claim. `onScrollBeginDrag`/`onScrollEndDrag` map onto the user-driven scroll _session_ (a touchpad's begin/end signal, or the grouped wheel burst) rather than a finger literally touching the content — the closest true statement available, since a touchpad never touches the content directly. The momentum pair reflects the adjustment actually continuing to move after the session ends rather than a generic \"decelerate\" signal that fires on every lift — a glide that stops dead reports the drag pair with no momentum pair, as RN does. None of this installs until a handler is attached: with all four phase callbacks attached, a scroll event costs 6.93&nbsp;µs versus 7.17&nbsp;µs with none attached — inside the noise; the GTK controller itself costs 0.31&nbsp;µs per event once any phase handler is present, and a begin/end consumer specifically adds 0.235&nbsp;µs per wheel detent for the session state machine. Scroll arbitration between a scroller and a child gesture is touch-only: `GtkScrolledWindow`'s own gestures are touch-only, so under a mouse a child pan never competes with scrolling at all. Two known edges under touch: a child gesture that claims on a move rather than on the initial press can lose the first ~8&nbsp;px to the scroller (GTK's claim is irrevocable, the same artifact iOS has); and a mouse wheel during an active gesture terminates the responder rather than being suppressed. **The scroller carries RN's own base style**, `flexGrow: 1, flexShrink: 1`, composed under the app's `style` the same way RN's `StyleSheet.compose` composes it, on the same node `style` lands on — `FlatList`, `SectionList` and `VirtualizedList` inherit it. This is what makes an unstyled scrollable a viewport rather than a box grown to its content, and it has one consequence worth knowing: an explicit main-axis `height` on the scroller is only its flex _basis_ — inside a taller flex parent, `flexGrow` still expands it past that height. That is parity with RN's own Yoga behavior, not a deviation. To bound the viewport, bound the _parent_ (`<View style={{ height: 200 }}><FlatList /></View>`, what an RN app already writes) or cancel the base style with `flexGrow: 0`.",
1841
+ },
1842
+ {
1843
+ doc: "docs/reference/components/flat-list.md",
1844
+ heading: "FlatList",
1845
+ text: "FlatList — GTK implementation: a windowed core over `ScrollView`. Supported: Virtualization (`estimatedItemSize` or `getItemLayout`, `windowSize`/ `initialNumToRender` as the primary scroll-performance knobs, `maxToRenderPerBatch`/`updateCellsBatchingPeriod`). `data`/`renderItem`/`keyExtractor`/`extraData`, `ItemSeparatorComponent`. `CellRendererComponent` — RN's per-cell wrapper. The list still hands it the cell's absolute `style` and the `onLayout` that measures it, and both must be applied, which is what `react-native-draggable-flatlist` builds its design on. `ListHeader`/`Footer`/`EmptyComponent`, `onEndReached(-Threshold)`. `onViewableItemsChanged`/`viewabilityConfig` (`ViewToken`). `inverted` — RN's chat semantics: the list opens at `data[0]` and stays pinned on prepend. `refreshing`/`onRefresh`, `horizontal`, `stickyHeaderIndices`. A ref exposing `scrollToIndex`/`scrollToItem`/`scrollToOffset` plus `scrollTo`/`scrollToEnd` (`FlatListHandle`) — the scroll half of a `ScrollView` ref, not the geometry half: a windowed list is a composite over `ScrollView` and owns no widget of its own, so measure the `ScrollView` or a cell instead.. Differences from RN: 1000 rows mount windowed in roughly 120&nbsp;ms. `windowSize` defaults to **11**, not RN's 5 — desktop has no mobile memory pressure, and a wider window means fewer mount-and-reflow bursts per scrolled pixel (measured: 21% less churn, late frames down from 10/s to 7.7/s). Rows beyond the visible ones mount `maxToRenderPerBatch` (10) at a time, every `updateCellsBatchingPeriod` (50)&nbsp;ms, so a flick or a long `scrollToOffset` fills its window over several frames instead of stalling one. There is no pull gesture — `onRefresh` is always app-triggered. An inverted list shorter than its viewport anchors to the top, not the bottom. `CellRendererComponent` does not apply to a sticky cell (`stickyHeaderIndices`), because pinning reorders the cell's real GTK widget — the sticky container has to _be_ the cell.",
1846
+ },
1847
+ {
1848
+ doc: "docs/reference/components/section-list.md",
1849
+ heading: "SectionList",
1850
+ text: "SectionList — GTK implementation: built on `FlatList`. Supported: `sections`, `renderSectionHeader`, sticky section headers by default (`stickySectionHeadersEnabled`).. Differences from RN: Viewability props are not exposed yet (section-aware `ViewToken`s are not implemented).",
1851
+ },
1852
+ {
1853
+ doc: "docs/reference/components/virtualized-list.md",
1854
+ heading: "VirtualizedList",
1855
+ text: "VirtualizedList — GTK implementation: the same windowed core. Supported: RN's opaque data-source shape over the same windowed core `FlatList` sits on — `data` is read only through `getItemCount(data)` and `getItem(data, index)`, both called lazily; only the rows the window actually mounts are ever asked for. Everything else matches [FlatList](flat-list.md), `CellRendererComponent` included.. Differences from RN: The accessors are optional here and required upstream — one component serves both the opaque-source and plain-array shapes, which is why `FlatList` needs no separate implementation. `scrollToItem` scans the source through `getItem`, as upstream does — an opaque source has no index to look up directly. Every `FlatList` difference above applies unchanged.",
1856
+ },
1857
+ {
1858
+ doc: "docs/reference/components/modal.md",
1859
+ heading: "Modal",
1860
+ text: "Modal — GTK implementation: a modal `GtkWindow` (a portal). Supported: `visible`, `onRequestClose` (Escape or the window's close button), `title`, `width`/`height`; independently resizable, with relayout.. Differences from RN: This is a real, separate desktop window rather than an overlay drawn above the current one. `transparent` and `animationType` are accepted and have no effect.",
1447
1861
  },
1448
1862
  ];
1449
1863
  //# sourceMappingURL=generated.js.map