diff --git a/config/docs.php b/config/docs.php
index 91a5f4dc..c1e7695d 100644
--- a/config/docs.php
+++ b/config/docs.php
@@ -86,7 +86,7 @@
'mobile-ui' => [
'name' => 'nativephp/mobile-ui',
'baseline' => '0.3',
- 'released_versions' => ['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7'],
+ 'released_versions' => ['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9'],
],
],
diff --git a/resources/views/docs/mobile/4/digging-deeper/gestures.md b/resources/views/docs/mobile/4/digging-deeper/gestures.md
index 36708d42..847b521b 100644
--- a/resources/views/docs/mobile/4/digging-deeper/gestures.md
+++ b/resources/views/docs/mobile/4/digging-deeper/gestures.md
@@ -35,6 +35,9 @@ Create one in PHP with `SharedValue::make($initial)`, bind it to a gesture, and
As the user drags, `$drag` updates on the UI thread and the bound props follow instantly — the card moves, fades,
and shrinks together, with no PHP involved during the gesture.
+To have a drag settle on set points when the finger lifts, like a row that slides aside to show its actions, give the
+gesture area [snap points](../edge-components/gesture-area#swipe-to-reveal).
+
### Formulas
Rather than bind the raw value everywhere, derive from it with a chainable formula. Each method returns a new
diff --git a/resources/views/docs/mobile/4/edge-components/gesture-area.md b/resources/views/docs/mobile/4/edge-components/gesture-area.md
index 63da331e..e8aa3b03 100644
--- a/resources/views/docs/mobile/4/edge-components/gesture-area.md
+++ b/resources/views/docs/mobile/4/edge-components/gesture-area.md
@@ -5,9 +5,9 @@ order: 225
## Overview
-Captures a vertical pan/drag gesture over its content and writes the translation to a bound
-[shared value](../digging-deeper/gestures), so the drag can drive animation on the UI thread with no PHP round-trip.
-Children render normally — gesture detection wraps the whole content frame.
+Captures drag and pinch gestures over its content and writes them to bound
+[shared values](../digging-deeper/gestures), so the gesture can drive animation on the UI thread with no PHP
+round-trip. Children render normally — gesture detection wraps the whole content frame.
@verbatim
```blade static
@@ -27,9 +27,22 @@ screen in your app and drag the card.
## Props
-- `pan-y` - A [`SharedValue`](../digging-deeper/gestures) that receives the vertical drag translation (required for
- the gesture to do anything). Bind it, then read it from animatable props (`translate-y`, `opacity`, `scale`, …)
- on the children.
+- `pan-y` - A [`SharedValue`](../digging-deeper/gestures) that receives the vertical drag translation. Bind it, then
+ read it from animatable props (`translate-y`, `opacity`, `scale`, …) on the children.
+- `pan-x` - The same for the horizontal drag translation. Bind one axis or both.
+- `pinch` - A `SharedValue` that receives the pinch scale factor, where `1` is the content's own size. Read it from
+ `scale` on the children.
+- `pinch-min` / `pinch-max` - The smallest and largest the pinch scale can go (optional, float). The limit is applied
+ as the fingers move, so the scale never runs past it and a pinch that changes direction responds straight away.
+- `bounded` - Keep zoomed content on screen (optional, boolean). See
+ [Zoomable images](#zoomable-images).
+- `content-ratio` - The content's width over height, for `bounded` (optional, float).
+ The content is taken to be that shape fitted inside the area, which is where an image with `fit="contain"` draws.
+ Leave it out when the content fills the area.
+- `snap-x` / `snap-y` - The points a bound pan settles on when the finger lifts
+ (optional), as an array of numbers or a comma-separated string. Give at least two. See
+ [Swipe to reveal](#swipe-to-reveal).
+- `swipe-fingers` - How many fingers a `@swipe` needs (optional, integer, default `1`).
+
+## Events
+
+- `@dragEnd` - Component method to call when the finger lifts after a drag.
+ It gets the final translation on each axis as two floats, `x` then `y`. A bound axis reports its shared value,
+ which with snap points is the point it settled on. An unbound axis reports how far the finger moved.
+- `@pinchEnd` - Component method to call when the fingers lift after a pinch, with the final scale as a float.
+- `@swipe` - Component method to call on a quick swipe, with its direction as a string: `left`, `right`, `up` or
+ `down`.
+
+## Scrolling and swipe back
+
+
+
+A gesture area shares its touches with the scroll view around it and with the system's back gesture:
+
+- A pan bound on one axis only takes drags along that axis, so a list of rows that each pan sideways still scrolls
+ up and down. On iOS, before mobile-ui 0.9, a one-axis pan took every touch from the moment the finger went down.
+- On iOS 26, which lets you swipe back from anywhere on a screen, a sideways drag that starts on a `pan-x` area or a
+ `@swipe` area moves the area and leaves the screen where it is. A swipe in from the left edge of the screen still
+ goes back.
+- A swipe across a pressable inside the area, or one around it, doesn't press it. On Android, before mobile-ui 0.9,
+ it pressed it as well.
+
+## Zoomable images
+
+
+
+With `bounded`, the gesture area keeps zoomed content inside its own edges, the way a photo viewer does:
+
+- Zoomed in, the content pans until its edges meet the area's edges, and no further.
+- On an axis where the zoomed content still fits, such as the height of a wide photo, it stays centered. At its own
+ size it fits both ways, so it doesn't move at all.
+- A drag past an edge moves with resistance, and the content springs back into place when the finger lifts.
+- A pinch zooms around the point between the fingers, and two fingers pan as they pinch.
+
+Bind both pan axes and the pinch, bind the content's transforms to the same values, and pass the image's shape as
+`content-ratio`:
+
+@verbatim
+```blade static
+
+
+
+```
+@endverbatim
+
+The shared values live on the component. Making fresh ones puts the image back at its own size, in the middle:
+
+```php
+use Native\Mobile\Edge\SharedValue;
+
+public ?SharedValue $zoom = null;
+public ?SharedValue $panX = null;
+public ?SharedValue $panY = null;
+
+public function showImage(): void
+{
+ $this->zoom = SharedValue::make(1.0);
+ $this->panX = SharedValue::make();
+ $this->panY = SharedValue::make();
+}
+```
+
+
+
+## Swipe to reveal
+
+
+
+With `snap-x` (or `snap-y`), a bound pan settles on set points, the way a row in a mail app slides aside to show its
+actions:
+
+- Between the lowest and highest point, the content follows the finger. Past them it moves a third as far as the
+ finger does.
+- When the finger lifts, the content springs to the nearest point. A flick counts by where it was heading, so a quick
+ flick opens a row the finger only moved part of the way.
+- `@dragEnd` gets the point it settled on.
+
+Here a card slides left to show a Reply button beside it. The button is there the whole time, shrunk to nothing, and
+grows in as the card moves:
+
+@verbatim
+```blade static
+@php $shown = $offset->interpolate([-48, 0], [1, 0]); @endphp
+
+
+
+
+ Swipe me left
+
+
+ @if ($open)
+
+
+
+ @else
+
+
+
+ @endif
+
+
+```
+@endverbatim
+
+The shared value lives on the component, so the card stays where it settled when the screen renders again. A fresh
+one each render would put it back at rest every time.
+
+PHP doesn't hear about the drag frame by frame, so `settled()` records where the card came to rest with `setValue()`.
+Setting a snapping pan's value from PHP springs the content there, so closing the card is one more `setValue()`:
+
+```php
+use Native\Mobile\Edge\SharedValue;
+
+public ?SharedValue $offset = null;
+
+public bool $open = false;
+
+public function mount(): void
+{
+ $this->offset = SharedValue::make();
+}
+
+public function settled(float $x, float $y): void
+{
+ $this->offset->setValue($x);
+ $this->open = $x < 0;
+}
+
+public function reply(): void
+{
+ $this->offset->setValue(0);
+ $this->open = false;
+
+ // ...
+}
+```
+
+