diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6da1b27..b1c1a0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ All notable changes follow Keep a Changelog.
confirmed.
- Added live pull metrics, lifecycle stress controls, browser guidance, and
published-package integration proofs to the documentation site.
+- Preserved the upward touch-pan direction at the top boundary so scrollable
+ content can begin scrolling normally from its first row.
## [0.1.0-alpha.0] - 2026-09-05
diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md
index 67313e7..176421b 100644
--- a/docs/INTEGRATIONS.md
+++ b/docs/INTEGRATIONS.md
@@ -39,15 +39,22 @@ element explicitly:
```tsx
-
+
+ {/* feed */}
+
```
+Make the root itself scrollable. Do not place a default PTR root—with its
+mechanical overflow containment—inside a second element that is expected to
+receive chained scrolling.
+
The documentation proof uses the published v5 package. Desktop automation
-verifies that the sheet opens, exposes a dedicated scroll root, and closes.
+verifies that the sheet opens, scrolls the dedicated root, and closes.
Combined drag feel on iOS and Android remains manual pending; the alpha does not
claim physical-device verification.
diff --git a/e2e/gesture.spec.ts b/e2e/gesture.spec.ts
index cf2f254..1c60411 100644
--- a/e2e/gesture.spec.ts
+++ b/e2e/gesture.spec.ts
@@ -77,6 +77,16 @@ test('documents ownership, native refresh, and truthful device status', async ({
test('renders real sibling primitive integration proofs', async ({ page }) => {
await page.goto('/#integrations')
+ await expect(
+ page.getByRole('link', { name: 'React Swipe Actions' }),
+ ).toHaveAttribute('href', 'https://react-swipe-actions.nipesolutions.com/')
+ await expect(
+ page.getByRole('link', { name: 'React Spring Bottom Sheet' }),
+ ).toHaveAttribute(
+ 'href',
+ 'https://react-spring-bottom-sheet.nipesolutions.com/',
+ )
+
const swipe = page.getByTestId('swipe-integration')
await expect(swipe).toHaveAttribute('data-integration', 'swipe-actions')
await expect(swipe.locator('.swipe-actions-root').first()).toBeVisible()
@@ -87,6 +97,32 @@ test('renders real sibling primitive integration proofs', async ({ page }) => {
await page.getByRole('button', { name: 'Close sheet proof' }).click()
})
+test('sheet content keeps the native scroll direction available at its top boundary', async ({
+ page,
+}) => {
+ await page.goto('/#integrations')
+ await page.getByRole('button', { name: 'Open bottom sheet proof' }).click()
+
+ const root = page.getByTestId('sheet-ptr')
+ await expect(root).toHaveAttribute('data-at-top', 'true')
+ const touchAction = await root.evaluate(
+ (node) => window.getComputedStyle(node).touchAction,
+ )
+ expect(['pan-x pan-up', 'pan-x pan-y']).toContain(touchAction)
+
+ const scroll = page.locator('.sheet-scroll')
+ const dimensions = await scroll.evaluate((node) => ({
+ clientHeight: node.clientHeight,
+ scrollHeight: node.scrollHeight,
+ }))
+ expect(dimensions.scrollHeight).toBeGreaterThan(dimensions.clientHeight)
+ await scroll.hover()
+ await page.mouse.wheel(0, 180)
+ await expect
+ .poll(() => scroll.evaluate((node) => node.scrollTop))
+ .toBeGreaterThan(0)
+})
+
test('Swipe Actions owns a horizontal row trace', async ({ page }) => {
await page.goto('/#integrations')
const row = page.locator('.swipe-actions-root').first()
diff --git a/src/core.css b/src/core.css
index 25f3d72..e9c0015 100644
--- a/src/core.css
+++ b/src/core.css
@@ -10,7 +10,7 @@
}
.ptr-root[data-at-top='true'] {
- touch-action: pan-x pan-down;
+ touch-action: pan-x pan-up;
}
.ptr-indicator {
diff --git a/website/src/demos.tsx b/website/src/demos.tsx
index 019521b..cd1850f 100644
--- a/website/src/demos.tsx
+++ b/website/src/demos.tsx
@@ -369,26 +369,26 @@ export function BottomSheetProof() {
Close sheet proof
-
-
- new Promise((resolve) => window.setTimeout(resolve, 400))
- }
- >
-
-
-
-
- {Array.from({ length: 8 }, (_, index) => (
-
- Sheet content {index + 1}
-
- ))}
-
-
-
+
+ new Promise((resolve) => window.setTimeout(resolve, 400))
+ }
+ >
+
+
+
+
+ {Array.from({ length: 8 }, (_, index) => (
+
+ Sheet content {index + 1}
+
+ ))}
+
+
diff --git a/website/src/main.tsx b/website/src/main.tsx
index a38c4e2..09939c6 100644
--- a/website/src/main.tsx
+++ b/website/src/main.tsx
@@ -183,7 +183,12 @@ function IntegrationProofs() {
-
Swipe Actions inside a feed
+
Horizontal motion reveals a row. Vertical motion stays with
scrolling. A downward pull at the top belongs to refresh.
@@ -191,7 +196,12 @@ function IntegrationProofs() {
-
Pull to Refresh inside a sheet
+
The sheet handle remains outside the scrolling feed. The feed
element is passed explicitly as the pull boundary.
diff --git a/website/src/site.css b/website/src/site.css
index 16dad9f..b38838d 100644
--- a/website/src/site.css
+++ b/website/src/site.css
@@ -574,6 +574,15 @@ pre {
.integrations .section-heading {
margin-bottom: 56px;
}
+.integrations h3 a {
+ color: inherit;
+ text-decoration-color: var(--tension-deep);
+ text-decoration-thickness: 2px;
+ text-underline-offset: 5px;
+}
+.integrations h3 a:hover {
+ color: var(--tension-deep);
+}
.integration-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));