/** * ScrollHeader — a screen header that hands over to a compact bar as the page * scrolls. * * A screen with a title has two states and every app draws both: the title * large, at rest, with room around it, and the title small, pinned, once you * are reading. Hand-rolling the change between them is where screens stop * matching each other — one snaps, one crossfades at a different point, one * forgets the bar is over a safe area — so the two states and the transit * between them are one component here. * * ```tsx * * * Library * * * * * * Library * 128 components * * {rows} * * ``` * * ## The band collapses; nothing inside it is animated up * * The header is one absolutely positioned band over the scroller, and the only * thing driven by the scroll is its height: `bar + max(0, large - offset)`. * The bar is anchored to its top and the large block to its bottom, so the * band shrinking is what carries the large block up and behind the bar, and * `overflow-hidden` is what cuts it off there. * * Driving the height rather than a `translateY` is what makes the rest fall * out for free. Over-scrolling makes `large - offset` larger than `large`, so * the band grows and a cover filling it stretches by being laid out bigger — * no scale transform, so a photograph stretches without going soft. And the * band's height is the inset the content needs, so there is one number rather * than two that have to agree. * * ## The two titles cross-fade; neither one morphs * * The large title and the bar title are separate elements that fade past each * other. A single title scaled and translated between the two positions tracks * beautifully until the text is long enough to truncate, at which point it is * animating between two different strings. * * Both are therefore in the tree at once, which is a problem for a screen * reader — one of them is invisible and would still be read. So the crossing * point is also published to React as `collapsed`, and whichever title is not * being shown is hidden from accessibility. That is one re-render per crossing * and no more: everything that runs per frame stays in shared values. * * ## The bar's surface is a fill, or a frost * * `surface` says what the bar is drawn on once it has taken over: a token * fill, nothing at all over a cover, or `blur` — a real material, so the rows * passing under the bar stay legible as shape and colour while losing the * detail that would compete with the title on top. * * The frost needs a native view, and there are two ways it cannot be drawn: * `expo-blur` is optional and may not be installed, and Reduce Transparency is * a preference that outranks the design. Both fall back to the plain * background token rather than to nothing, because a bar you cannot read is a * worse answer than a bar that is not frosted. * * ## What it needs * * A height to fill, and exactly one scrollable child. The child is cloned with * the scroll handler and the content inset composed onto it, the same way * `ScrollFade` wraps one, so a `ScrollView`, a `FlatList` or a `SectionList` * all work unchanged. */ import { type ReactNode } from 'react'; import { View, type ImageSourcePropType, type Text as RNText, type ViewProps } from 'react-native'; import { type SharedValue } from 'react-native-reanimated'; import { type VariantProps } from 'tailwind-variants'; import { type TextProps } from '../../primitives/text.js'; /** * The scrollables Reanimated already animates. Its animated components are * ordinary function components carrying the *inner* component's name, so there * is nothing on one to test — but these two are module-level constants, and * identity is exact. */ /** Which way the frost tints. `default` follows the app's theme. */ export type ScrollHeaderMaterial = 'light' | 'dark' | 'default'; declare const scrollHeaderVariants: import("tailwind-variants").TVReturnType<{ /** What the bar is drawn on once it has taken over. */ surface: { plain: { barSurface: string; }; muted: { barSurface: string; }; none: { barSurface: string; }; blur: { barSurface: string; }; }; divider: { true: { barSurface: string; }; false: {}; }; }, { root: string; band: string; cover: string; bar: string; barSurface: string; large: string; actions: string; }, undefined, { /** What the bar is drawn on once it has taken over. */ surface: { plain: { barSurface: string; }; muted: { barSurface: string; }; none: { barSurface: string; }; blur: { barSurface: string; }; }; divider: { true: { barSurface: string; }; false: {}; }; }, { root: string; band: string; cover: string; bar: string; barSurface: string; large: string; actions: string; }, import("tailwind-variants").TVReturnType<{ /** What the bar is drawn on once it has taken over. */ surface: { plain: { barSurface: string; }; muted: { barSurface: string; }; none: { barSurface: string; }; blur: { barSurface: string; }; }; divider: { true: { barSurface: string; }; false: {}; }; }, { root: string; band: string; cover: string; bar: string; barSurface: string; large: string; actions: string; }, undefined, unknown, unknown, undefined>>; type ScrollHeaderVariantProps = VariantProps; /** What the bar is drawn on once the large block has gone. */ export type ScrollHeaderSurface = NonNullable; /** Which half of the header a part is standing in. */ export type ScrollHeaderSlot = 'bar' | 'large'; export interface ScrollHeaderProps extends ViewProps { className?: string; /** * Height of the pinned bar in points, before the device's top inset. The * inset is added on top of this rather than taken out of it, so the bar's * contents keep this much room on every device. */ barHeight?: number; /** * How much of the large block has to leave before the bar has fully taken * over, as a fraction of its height. Below 1 the crossing happens early, * which suits a tall block whose last few points are not worth waiting for. */ threshold?: number; /** * Settle a part-scrolled band open or closed when the finger lifts, rather * than leaving the header half-collapsed. */ snap?: boolean; /** * Let the band grow past its resting height when the scroller is pulled * down. A cover fills the band, so this is what stretches it. Off under * Reduce Motion. */ stretch?: boolean; /** Add the device's top inset above the bar. Off inside a screen that already has one. */ inset?: boolean; /** * Called as the bar takes over, and again when the large block comes back. * Fires on the crossing, not on every frame. */ onCollapsedChange?: (collapsed: boolean) => void; /** * A shared value to mirror the collapse into, 0 to 1, for animating * something outside the header against the same transition. */ progress?: SharedValue; /** The parts, and exactly one scrollable. */ children?: ReactNode; } export interface ScrollHeaderBarProps extends ViewProps { className?: string; /** * What the bar is drawn on once it has taken over. `none` leaves it clear, * for a bar over a cover that should stay visible. `blur` frosts it, so the * content passing under the bar stays legible as shape and colour. * * `blur` needs `expo-blur`, which is optional, and it is replaced by an * opaque bar under Reduce Transparency. Both fall back to `plain` — a bar * whose title cannot be read is a worse answer than one that is not frosted. */ surface?: ScrollHeaderSurface; /** A hairline under the bar, drawn with its surface. */ divider?: boolean; /** * Depth of the frost, on `expo-blur`'s 0–100 scale. Defaults to 40 — heavier * than a scrim's, because a scrim covers a whole screen and this is a thin * band read against content moving under it. Ignored unless `surface` is * `blur`. */ intensity?: number; /** * Which way the frost tints. Defaults to the app's theme rather than the * device's, so an app running light inside a dark OS frosts light. Ignored * unless `surface` is `blur`. */ material?: ScrollHeaderMaterial; children?: ReactNode; } export interface ScrollHeaderLargeProps extends ViewProps { className?: string; children?: ReactNode; } export interface ScrollHeaderActionsProps extends ViewProps { className?: string; children?: ReactNode; } export interface ScrollHeaderCoverProps extends ViewProps { className?: string; /** A picture behind the header. Laid out to fill the band, so it stretches with it. */ source?: ImageSourcePropType; /** * The gradient drawn when there is no picture, or under one that has not * loaded. Defaults to two of the theme's series tokens, so an app that puts * its charts on brand puts this on brand with them. */ colors?: [string, string, ...string[]]; /** A wash over the cover, so a title stays legible on a bright picture. */ scrim?: boolean; children?: ReactNode; } export declare const ScrollHeader: import("react").ForwardRefExoticComponent> & { Bar: import("react").ForwardRefExoticComponent>; Large: import("react").ForwardRefExoticComponent>; Title: import("react").ForwardRefExoticComponent>; Description: import("react").ForwardRefExoticComponent>; Actions: import("react").ForwardRefExoticComponent>; Cover: import("react").ForwardRefExoticComponent>; }; export {}; //# sourceMappingURL=index.d.ts.map