Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Full documentation lives here:
## What it helps with

- Scroll-driven animated headers
- Ready-made, preset-driven collapsible headers via `Collapsible` and `CollapsibleTabs`
- Shared header state across tabs, pagers, and multiple scrollables
- Navigation-rendered headers in Expo Router or React Navigation
- Custom scrollables via `createHeaderMotionScrollable()` and `ScrollablePresets`
Expand Down Expand Up @@ -102,6 +103,32 @@ function AppHeader() {

In a real header, use `useMotionProgress()` to drive your Reanimated styles. See the [Quick Start](https://pawicao.github.io/react-native-header-motion/docs/quick-start) for the full walkthrough, animation examples, and styling details.

## High-level API

If you'd rather not hand-write the choreography, `Collapsible` wraps the same primitives into a preset-driven compound component — and `CollapsibleTabs` does the same for one header shared across pager pages:

```tsx
import { Collapsible } from 'react-native-header-motion';
import { Stack } from 'expo-router';

export default function Screen() {
return (
<Collapsible preset="parallax">
<Collapsible.NavigationHeader
render={(header) => <Stack.Screen options={{ header: () => header }} />}
>
<Collapsible.Pinned>{/* stays in place */}</Collapsible.Pinned>
<Collapsible.Dynamic>{/* collapses away */}</Collapsible.Dynamic>
</Collapsible.NavigationHeader>

<Collapsible.ScrollView>{/* content */}</Collapsible.ScrollView>
</Collapsible>
);
}
```

See the [high-level API docs](https://pawicao.github.io/react-native-header-motion/docs/high-level/collapsible) for presets, tabs, and pager adapters.

## Version notes

- Upgrading from `v0.3.x`? Read [MIGRATION-v1.md](./MIGRATION-v1.md).
Expand Down
103 changes: 103 additions & 0 deletions docs/docs/high-level/collapsible-tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
sidebar_position: 3
title: CollapsibleTabs
---

# CollapsibleTabs

`CollapsibleTabs` shares one collapsible header across multiple swipeable pages. It absorbs everything the low-level [multi-tab setup](../guides/multiple-tabs-pages) wires by hand: the `useActiveScrollId()` state, the per-scrollable `scrollId`s, and the pager coordination.

```tsx
import { Collapsible, CollapsibleTabs } from 'react-native-header-motion';

<CollapsibleTabs tabs={['posts', 'about']} preset="parallax">
<Collapsible.Header>
<Collapsible.Pinned><TitleRow /></Collapsible.Pinned>
<Collapsible.Dynamic><Hero /></Collapsible.Dynamic>
<CollapsibleTabs.Bar />
</Collapsible.Header>

<CollapsibleTabs.Pager>
<CollapsibleTabs.Tab name="posts">
<Collapsible.FlatList data={posts} renderItem={renderPost} />
</CollapsibleTabs.Tab>
<CollapsibleTabs.Tab name="about">
<Collapsible.ScrollView>{about}</Collapsible.ScrollView>
</CollapsibleTabs.Tab>
</CollapsibleTabs.Pager>
</CollapsibleTabs>
```

Note what's absent: no `scrollId` props, no active-id state, no pager event wiring. Each `CollapsibleTabs.Tab` provides its `name` as the default `scrollId` through context, so **any** header-motion scrollable inside it — including custom ones from `createHeaderMotionScrollable()` — participates automatically. An explicit `scrollId` prop still wins if you need to override.

## Parts

- **`CollapsibleTabs`** — the root. Extends [`Collapsible`](./collapsible) (same `preset` and pass-through props) and owns the active-tab state.
- **`CollapsibleTabs.Pager`** — renders the pages through a pager adapter and keeps it in sync with the active tab.
- **`CollapsibleTabs.Tab`** — one page; provides its `name` as the default `scrollId`.
- **`CollapsibleTabs.Bar`** — a minimal, deliberately unstyled tab bar. For a custom one, build on `useCollapsibleTabs()`.

All of it works inside `Collapsible.NavigationHeader` too — the tabs context is bridged across the navigation boundary automatically, so `CollapsibleTabs.Bar` can live in a navigation-rendered header.

## Props

### `CollapsibleTabs`

Everything from `Collapsible` except `activeScrollId` (owned internally), plus:

| Prop | Type | Default | Description |
| ------------- | --------------------------------------------- | ---------------- | ----------------------------------------------------------------------- |
| `tabs` | `(string \| { name, label? })[]` | — | The tabs in pager order. Labels are what `CollapsibleTabs.Bar` renders. |
| `initialTab` | `string` | first tab | Name of the initially active tab. |
| `onTabChange` | `(name: string) => void` | — | Called when the active tab changes, from a swipe or `goTo`. |

### `CollapsibleTabs.Pager`

| Prop | Type | Default | Description |
| --------- | ----------------------------- | ---------------- | ---------------------------------------- |
| `adapter` | `CollapsibleTabsPagerAdapter` | built-in pager | The pager engine. See below. |
| `style` | `ViewStyle` | `{ flex: 1 }` | Style for the pager container. |

## Pager engines

The pager is pluggable through a tiny adapter interface, so the library itself has **zero pager dependencies**.

**Built-in default** — a paging horizontal `ScrollView`. No extra install, keeps all pages mounted, supports swipe and programmatic changes.

**`react-native-pager-view`** — for native pager behavior, hand your app's `PagerView` to the factory. The library never imports the package itself, so it stays a dependency of *your* app:

```tsx
import PagerView from 'react-native-pager-view';
import { createPagerViewAdapter } from 'react-native-header-motion';

const pagerAdapter = createPagerViewAdapter(PagerView);

<CollapsibleTabs.Pager adapter={pagerAdapter}>
```

**Custom** — implement `CollapsibleTabsPagerAdapter`: a component receiving `{ initialIndex, onIndexChange, controllerRef, style, children }`. Render `children` (one element per tab), report user-driven page changes through `onIndexChange`, and assign `{ setIndex }` to `controllerRef` for programmatic changes.

## Custom tab bars

`useCollapsibleTabs()` exposes the tab state anywhere in the tree:

```tsx
import { useCollapsibleTabs } from 'react-native-header-motion';

function MyTabBar() {
const { tabs, activeTab, goTo } = useCollapsibleTabs();

return (
<View style={styles.row}>
{tabs.map((tab) => (
<TabButton
key={tab.name}
label={tab.label}
isActive={tab.name === activeTab}
onPress={() => goTo(tab.name)}
/>
))}
</View>
);
}
```
114 changes: 114 additions & 0 deletions docs/docs/high-level/collapsible.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
sidebar_position: 1
title: Collapsible
---

# Collapsible

`Collapsible` is the high-level, preset-driven way to build a collapsible header. It wraps the `HeaderMotion` primitives — the provider, the measured header, the animated styles — into a compound component where you compose the header from explicit parts and pick a [preset](./presets) for the animation.

Everything `Collapsible` does is built on the public low-level API, and every low-level prop passes through. You can start high-level and drop down at any point.

## Anatomy

```tsx
import { Collapsible } from 'react-native-header-motion';
import { Stack } from 'expo-router';

export default function Screen() {
return (
<Collapsible preset="parallax">
<Collapsible.NavigationHeader
render={(header) => <Stack.Screen options={{ header: () => header }} />}
>
<Collapsible.Pinned>
<TitleRow />
</Collapsible.Pinned>
<Collapsible.Dynamic>
<Hero />
</Collapsible.Dynamic>
<SearchBar />
</Collapsible.NavigationHeader>

<Collapsible.ScrollView>{content}</Collapsible.ScrollView>
</Collapsible>
);
}
```

Each part names the *role* a piece of UI plays while the header collapses:

- **`Collapsible`** — the root provider. Renders a `HeaderMotion` provider, resolves the `preset`, and shares both with the parts. No visual output.
- **`Collapsible.Header`** — the header frame. Measures the total header height (like `HeaderMotion.Header`) and slides up by the collapse distance as the user scrolls.
- **`Collapsible.Pinned`** — content that stays visually in place (a title row, actions). It counter-translates against the frame's slide.
- **`Collapsible.Dynamic`** — the collapsing section. Its measured height defines the collapse distance (like `HeaderMotion.Header.Dynamic`), and the active preset animates its content. Anything after it (like the `SearchBar` above) simply rides up and ends docked under the pinned content.
- **`Collapsible.NavigationHeader`** — a `Collapsible.Header` rendered by a navigation library. It fuses the `Bridge` / `NavigationBridge` wiring into one component: compose the header as children, and use `render` to place the prepared element into your navigator.
- **`Collapsible.ScrollView` / `Collapsible.FlatList`** — the same pre-wired scrollables as `HeaderMotion.ScrollView` / `HeaderMotion.FlatList`, re-exported for a self-contained import surface. Custom scrollables from `createHeaderMotionScrollable()` work here too.

## Overlay headers without navigation

If your header is not rendered by a navigation library, use `Collapsible.Header` directly — it overlays the content by default, exactly like the low-level `HeaderMotion.Header`:

```tsx
<Collapsible preset="fade">
<Collapsible.Header style={styles.header}>
<Collapsible.Pinned>
<TitleRow />
</Collapsible.Pinned>
<Collapsible.Dynamic>
<Hero />
</Collapsible.Dynamic>
</Collapsible.Header>
<Collapsible.ScrollView>{content}</Collapsible.ScrollView>
</Collapsible>
```

## Props

### `Collapsible`

Accepts every `HeaderMotion` prop (`progressThreshold`, `measureDynamic`, `measureDynamicMode`, `activeScrollId`, `progressExtrapolation`), plus:

| Prop | Type | Default | Description |
| --------------- | ------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------- |
| `preset` | `CollapsiblePresetInput` | `'collapse'` | How the collapsing content animates. See [Presets](./presets). |
| `onStateChange` | `(state: 'expanded' \| 'collapsed') => void` | — | Called when the header settles into a terminal state. |

### `Collapsible.Header` / `Collapsible.NavigationHeader`

Accept all `HeaderMotion.Header` props except `asChild` (`overlay`, `pannable`, `panDecayConfig`, `withGestureHandlerRootView`, any `Animated.View` prop). `Collapsible.NavigationHeader` additionally requires:

| Prop | Type | Description |
| -------- | ------------------------------------- | -------------------------------------------------------------- |
| `render` | `(header: ReactElement) => ReactNode` | Places the prepared, context-bridged header into your navigator. |

### `Collapsible.Dynamic`

Accepts all `HeaderMotion.Header.Dynamic` props except `asChild`, plus:

| Prop | Type | Description |
| -------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `contentStyle` | `Animated.View` `style` | Style for the inner content view the preset effects animate. Layout styles for the measured wrapper go on `style`. |

The wrapper clips its content by default (`overflow: 'hidden'`); override it through `style` if needed.

### `Collapsible.Pinned`

Accepts all `Animated.View` props.

## Imperative control

`useCollapsibleHeader()` exposes the motion state plus imperative controls, anywhere inside the tree (it also works in plain `HeaderMotion` trees):

```tsx
import { useCollapsibleHeader } from 'react-native-header-motion';

const { progress, progressThreshold, collapse, expand } = useCollapsibleHeader();

collapse(); // scrolls the active scrollable until the header is collapsed
expand({ animated: false });
```

## Pull to refresh

The scrollables accept `refreshControl` (and `refreshing` / `onRefresh`) exactly like their low-level counterparts — see [Pull to refresh](../guides/pull-to-refresh). Custom refresh indicators compose naturally into the header as regular children of `Collapsible.Header`.
73 changes: 73 additions & 0 deletions docs/docs/high-level/presets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
sidebar_position: 2
title: Presets
---

# Presets

A preset describes how the collapsing content of a [`Collapsible`](./collapsible) header animates. The structural choreography — the frame sliding up, pinned content counter-translating — is intrinsic to the parts; presets only decorate the collapsing section, which is what makes them freely composable.

## Built-in presets

Pass a name for the defaults, or call the factory from `CollapsiblePresets` to configure:

```tsx
import { Collapsible, CollapsiblePresets } from 'react-native-header-motion';

<Collapsible preset="collapse"> // string shorthand
<Collapsible preset={CollapsiblePresets.parallax({ factor: 0.3 })}> // configured
```

| Preset | Effect | Options |
| ---------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `collapse` | Content slides up and is clipped under the pinned sections (the classic iOS large-title feel). | — |
| `fade` | Content fades out in place while the header collapses. | `from` (default `0`), `to` (default `0.8`) |
| `parallax` | Content lags behind the collapse and fades out. | `factor` (default `0.5`), `fade` (default `true`) |
| `scale` | Content shrinks and fades out. | `to` (default `0.9`), `fade` (default `true`) |
| `none` | No content effect; the header still slides up. | — |

## Combining presets

Pass an array to combine effects. `transform` arrays concatenate in order, numeric `opacity` values multiply, and any other property is taken from the last preset that defines it:

```tsx
<Collapsible preset={['collapse', CollapsiblePresets.fade({ to: 0.5 })]}>
```

## Custom presets

A preset is a worklet from the current motion state to per-part styles. Author one with `createCollapsiblePreset()` — the function runs on the UI thread, so it **must carry the `'worklet'` directive**:

```tsx
import { createCollapsiblePreset } from 'react-native-header-motion';

const lift = createCollapsiblePreset(({ progress, progressThreshold }) => {
'worklet';
return {
header: { borderBottomWidth: progress },
dynamicContent: {
opacity: 1 - progress,
transform: [{ translateY: -progress * progressThreshold * 0.25 }],
},
};
});

<Collapsible preset={lift}>
```

The returned object may style four parts:

| Key | Applied to | Merged on top of |
| ---------------- | --------------------------------------------- | ------------------------------------ |
| `header` | `Collapsible.Header` frame | The intrinsic slide-up transform |
| `pinned` | Every `Collapsible.Pinned` | The intrinsic counter-translate |
| `dynamic` | The measured `Collapsible.Dynamic` wrapper | Its default `overflow: 'hidden'` |
| `dynamicContent` | The content view inside `Collapsible.Dynamic` | — |

Use transforms only on `dynamic` — its layout defines the collapse distance, and transforms don't affect layout measurement.

Because presets are plain values, they are shareable: hoist them to module scope (or memoize) so the animated styles aren't rebuilt on every render, and publish the ones you're proud of.

:::note Designed to grow
The preset context (`{ progress, progressThreshold }`) is intentionally open-ended. When the headless refresh control lands, refresh state will join it, letting presets react to pull-to-refresh without any breaking change. Read the fields you need and ignore the rest.
:::
10 changes: 10 additions & 0 deletions docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ const sidebars: SidebarsConfig = {
id: 'quick-start',
label: 'Quick Start',
},
{
type: 'category',
label: 'High-level API',
collapsed: false,
items: [
'high-level/collapsible',
'high-level/presets',
'high-level/collapsible-tabs',
],
},
{
type: 'category',
label: 'Guides',
Expand Down
Loading
Loading