/** * Panelside — a navigation panel that moves the app aside instead of covering * it. * * ```tsx * * * * * * * * Recents * * * * * * * * * * * * * * ``` * * ## Why it is not a Drawer * * A drawer is an overlay: it mounts through a portal, lands above everything * and dims what it hid. That is the wrong shape here, because the whole point * of this pattern is that the app *stays legible* — it slides across, shrinks, * rounds its corners and waits, so the panel reads as a layer behind the app * rather than a sheet on top of it. A portal cannot do that: its content is * above the app content by construction, and the app content is somewhere else * in the tree entirely, unreachable. * * So Panelside owns both halves. It renders inline, keeps the panel and the * app screen as siblings under one clipping container, and gives them a single * `progress` value to move against. `Panelside.Scene` is the wrapper you put * around your own screen; without it there is nothing to push, which is why it * is explicit rather than inferred. * * ## The scene maths * * React Native scales a view about its centre, so a scene scaled to `s` has * already pulled its left edge `W * (1 - s) / 2` inward before any translation * is applied. Translating by the panel width alone would therefore leave a gap * that grows with the scale, and the panel would look mis-measured. Subtracting * that inset is what puts the scene's *visible* edge exactly where the panel * ends: * * scale = 1 - (1 - s) * p * translateX = p * (width + gap) - W * (1 - scale) / 2 * * Both are driven from one shared value, so a half-finished drag is a real * halfway state rather than an interpolation between two snapshots. * * ## One gesture, both directions * * A single pan opens and closes. By default it listens across the whole * surface, because that is the behaviour this pattern is known for: a sideways * drag anywhere on the app brings the panel in, from wherever your thumb * already was. What keeps a list usable underneath it is the pair of * thresholds — the drag gives itself up on twelve points of vertical travel * and only claims the touch at fourteen horizontal, so anything even slightly * vertical resolves as a scroll. * * `swipeFrom="edge"` narrows the closed-state hit area to a strip at the * leading screen edge instead. That is for a scene with its own use for a * horizontal drag — a carousel, a wide table, a pannable chart — which would * otherwise fight the panel and lose. * * Reanimated's default `ReduceMotion.System` applies throughout: with the * accessibility setting on, every spring here resolves instantly to its target * rather than travelling. */ import { type ReactElement, type ReactNode } from 'react'; import { type PressableProps, type ScrollViewProps, type TextInputProps, type ViewProps } from 'react-native'; import { type SharedValue } from 'react-native-reanimated'; import { type MenuContentProps } from '../menu/index.js'; import { type TextProps } from '../../primitives/text.js'; export type PanelsideMode = 'push' | 'overlay'; export type PanelsideSwipeFrom = 'anywhere' | 'edge'; export type PanelsideItemSize = 'default' | 'sm'; export type PanelsideCtaSize = 'default' | 'lg' | 'xl'; /** * What a header or a footer paints behind itself. * * `transparent` paints nothing, and the list runs the full height of the panel * underneath it. `fade` dissolves the list into the panel background over the * strip above the controls. `solid` is a band with an edge on it, for a footer * that is a row of the layout rather than something floating over one. */ export type PanelsideSurface = 'transparent' | 'fade' | 'solid'; /** How the panel's small round controls are drawn: a fill, or a ring. */ export type PanelsideControlVariant = 'filled' | 'outline'; export interface UsePanelsideResult { open: boolean; setOpen: (open: boolean) => void; toggle: () => void; /** * How far the panel has travelled, 0 to 1, on the UI thread. Read it to move * something of your own with the panel — a header that fades, a title that * slides — without a re-render per frame. */ progress: SharedValue; /** True while the panel is docked open beside the scene. */ docked: boolean; /** The page the scene is showing — a `Panelside.Page`'s `value`. */ route: string; /** Go to a page, closing the panel on the way. */ navigate: (route: string) => void; } /** * The panel's state, from anywhere inside a `` — including your own * screen inside `Panelside.Scene`, which is where a custom open button usually * lives. */ export declare function usePanelside(): UsePanelsideResult; export interface PanelsideProps { children: ReactNode; /** Open state, when you want to own it. Pair with `onOpenChange`. */ open?: boolean; /** Called with the next open state, whether a gesture or you caused it. */ onOpenChange?: (open: boolean) => void; /** Open state to start at when you are not controlling it. */ defaultOpen?: boolean; /** * How the two layers relate. `push` moves the scene aside and curves it, * which is the point of this component. `overlay` slides the panel over a * scene that stays put — the same navigation, for a screen whose content * cannot afford to move. */ mode?: PanelsideMode; /** * Panel width in points. Defaults to 80% of the container capped at 360, * and to a third of it capped at 320 once docked — an overlay panel gives * the width back when it closes and a docked one keeps it, so they are not * the same measurement. The caps are what stop a tablet getting a navigation * list with a field of whitespace beside it. */ width?: number; /** * Container width at or above which the panel stops being an overlay and * becomes a permanent sidebar: laid out beside the scene, always open, with * the gesture and the trigger switched off. A docked panel also narrows to a * third of the container, capped at 320 — docked, every point it takes is a * point the app does not get back. * * Off by default, and deliberately not a guess — a large phone in landscape * is wider than a small tablet in portrait, so no single number is right for * every app. Set it high enough that what is left over is still a screen: * around 700 is the first width where both halves have room. */ dock?: number | false; /** Swipe to open, and drag the scene to close. Default true. */ swipeEnabled?: boolean; /** * Where a swipe may begin. `anywhere` is the default and the behaviour this * pattern is known for — a sideways drag across the app opens the panel from * wherever your thumb already was. * * `edge` narrows it to a strip at the leading screen edge, for a scene that * has its own use for a horizontal drag: a carousel, a wide table, a chart * you can pan. Anything like that under an `anywhere` panel will fight it, * and the panel usually wins. */ swipeFrom?: PanelsideSwipeFrom; /** * How wide the leading-edge strip that starts a swipe is, when `swipeFrom` * is `edge`. Default 48 — wider than the system's own edge gestures, because * there is no bezel to feel for. Ignored otherwise. */ edgeWidth?: number; /** * Tapping the pushed scene, or the Android back button, closes the panel. * Default true. */ dismissible?: boolean; /** * A tick under the finger when a swipe commits to opening or closing. Off by * default — needs the optional `expo-haptics`, and is silent without it. * * It fires on the commit rather than during the drag: the panel following * your thumb is already the feedback for the drag, and a tick per frame is * what makes a gesture feel broken rather than responsive. */ haptics?: boolean; /** * How far the scene shrinks at full open, as a scale factor. Sets the * default for every `Panelside.Scene` underneath; the scene's own prop still * wins. Here so the three numbers that describe the curve can be set once * where the panel is configured, rather than on a part further down. */ scale?: number; /** The corner radius the scene reaches at full open, in points. */ radius?: number; /** How far the scene is dimmed at full open, 0 to 1. */ dim?: number; /** * Which page the scene is showing. Controlled; pair it with `onRouteChange`. * * A route is any string you choose. It is matched against * `Panelside.Page`'s `value` and against `Panelside.Item`'s `to`, so a row * marks itself as the current destination and the scene swaps to the page * without either being wired to the other. */ route?: string; /** Which page the scene starts on, when the panel is not controlling `route`. */ defaultRoute?: string; /** Called with the route a row navigated to. */ onRouteChange?: (route: string) => void; className?: string; } declare function PanelsideRoot({ children, open: controlledOpen, onOpenChange, defaultOpen, mode, width: widthProp, dock, swipeEnabled, swipeFrom, edgeWidth, dismissible, haptics, scale, radius, dim, route: controlledRoute, defaultRoute, onRouteChange, className, }: PanelsideProps): import("react").JSX.Element; export interface PanelsidePanelProps extends ViewProps { className?: string; children?: ReactNode; } declare function PanelsidePanel({ className, children, style, ...props }: PanelsidePanelProps): import("react").JSX.Element; declare namespace PanelsidePanel { var displayName: string; } export interface PanelsideHeaderProps extends ViewProps { className?: string; /** Rendered as the heading. Omit it and supply your own in `children`. */ title?: string; /** A single element pinned to the trailing end of the title row. */ action?: ReactNode; /** * What the header paints behind itself. * * `transparent` is the default and paints nothing, so the header is the * panel's own surface with a title on it rather than a bar sitting on top of * one. In the panel's normal stacking that is the whole story — the header * takes a row and the list starts below it. * * `fade` and `solid` are for a header the caller has lifted out of that * stack — `className="absolute start-0 end-0 top-0"` — so the list runs * underneath it. They are the two shapes `Panelside.Footer` offers, drawn * the other way up. */ surface?: PanelsideSurface; /** Anything below the title row — a search field, a workspace switcher. */ children?: ReactNode; } declare function PanelsideHeader({ className, title, action, surface, children, style, ...props }: PanelsideHeaderProps): import("react").JSX.Element; declare namespace PanelsideHeader { var displayName: string; } export interface PanelsideSearchProps extends TextInputProps { className?: string; containerClassName?: string; } /** * A compact filter field for the panel. * * Deliberately not the library's `Input`: that field carries a label, a * description, an error slot and keyboard avoidance, none of which a panel * search row wants, and all of which would have to be switched off at every * call site. */ declare function PanelsideSearch({ className, containerClassName, placeholder, ...props }: PanelsideSearchProps): import("react").JSX.Element; declare namespace PanelsideSearch { var displayName: string; } export interface PanelsideContentProps extends ScrollViewProps { className?: string; contentContainerClassName?: string; children?: ReactNode; } declare function PanelsideContent({ className, contentContainerClassName, contentContainerStyle, children, ...props }: PanelsideContentProps): import("react").JSX.Element; declare namespace PanelsideContent { var displayName: string; } export interface PanelsideGroupProps extends ViewProps { className?: string; children?: ReactNode; } declare function PanelsideGroup({ className, children, ...props }: PanelsideGroupProps): import("react").JSX.Element; declare namespace PanelsideGroup { var displayName: string; } export interface PanelsideGroupLabelProps extends ViewProps { className?: string; children?: ReactNode; } declare function PanelsideGroupLabel({ className, children, ...props }: PanelsideGroupLabelProps): import("react").JSX.Element; declare namespace PanelsideGroupLabel { var displayName: string; } export interface PanelsideItemProps extends Omit { className?: string; /** * Leading element — an icon, an avatar, a coloured dot. The shorthand for * `Panelside.ItemIcon`. */ icon?: ReactNode; /** * The row's text, truncated to one line since chat titles run long. The * shorthand for `Panelside.ItemLabel`. */ label?: string; /** * The page this row goes to — a `Panelside.Page`'s `value`. * * Pressing it sets the panel's route, and the row marks itself active while * that route is the current one. It also closes the panel, since the thing * you just moved to would otherwise be behind the thing you moved from. * * `active` and `onPress` still win where they are passed, so a row can * navigate and do something else as well. */ to?: string; /** Leave the panel open after navigating. Off by default. */ closeOnNavigate?: boolean; /** Marks the row as the current destination. Derived from `to` when given. */ active?: boolean; /** * Trailing count or status. A number or string renders as a pill; anything * else renders as given. The shorthand for `Panelside.ItemBadge`. */ badge?: ReactNode; disabled?: boolean; /** * Row density. `sm` tightens the padding for a panel that has to show more * history at once, without touching the type size — a list you can read is * worth more than two extra rows. */ size?: PanelsideItemSize; /** * The row's contents, written out: `Panelside.ItemIcon`, * `Panelside.ItemLabel`, `Panelside.ItemBadge` and `Panelside.Action`, in * whatever order the row wants them. Anything else you draw works too. * * Children and the shorthand props compose — a row can take its label from * `label` and still write a trailing `Panelside.Action` as a child. */ children?: ReactNode; } /** * One destination, or one conversation. * * There are two ways to fill it, and they are the same row. The shorthand — * `icon`, `label`, `badge` — covers the row every navigation panel has, and is * what most call sites should use. The parts cover everything else: two lines * of text, a label that is not a string, a badge before the label rather than * after it, a trailing control that is not an overflow menu. * * ```tsx * } label="Inbox" badge={12} /> * * * * {thread.title} * {thread.unread} * * * ``` */ declare function PanelsideItem({ className, icon, label, to, closeOnNavigate, active: activeProp, badge, disabled, size, onPress, children, ...props }: PanelsideItemProps): import("react").JSX.Element; declare namespace PanelsideItem { var displayName: string; } export interface PanelsideItemIconProps extends ViewProps { className?: string; children?: ReactNode; } /** * The leading slot on a row. * * Whatever is inside it inherits the row's own tint rather than each call site * passing a colour that stops being right the moment the row goes active. */ declare function PanelsideItemIcon({ className, children, ...props }: PanelsideItemIconProps): import("react").JSX.Element; declare namespace PanelsideItemIcon { var displayName: string; } export interface PanelsideItemLabelProps extends TextProps { className?: string; children?: ReactNode; } /** * The row's text. It takes the flexible middle, so a long title truncates * rather than pushing the badge and the action off the end of the panel. */ declare function PanelsideItemLabel({ className, children, ...props }: PanelsideItemLabelProps): import("react").JSX.Element; declare namespace PanelsideItemLabel { var displayName: string; } export interface PanelsideItemBadgeProps extends ViewProps { className?: string; children?: ReactNode; } /** * The trailing count or status on a row. Text becomes a pill; anything else is * drawn as given, so a dot or a chip needs no opting out of the pill. */ declare function PanelsideItemBadge({ className, children, ...props }: PanelsideItemBadgeProps): import("react").JSX.Element; declare namespace PanelsideItemBadge { var displayName: string; } export interface PanelsideActionProps extends Omit { className?: string; /** * What a screen reader announces. The default control is an unlabelled glyph, * so this is the only description it has. */ label?: string; /** Replaces the default overflow glyph. */ children?: ReactNode; } declare function PanelsideAction({ className, label, children, onPress, ...props }: PanelsideActionProps): import("react").JSX.Element; declare namespace PanelsideAction { var displayName: string; } export interface PanelsideItemActionsProps { className?: string; /** * What a screen reader announces for the button. The control is an * unlabelled glyph, so this is the only description it has. */ label?: string; /** Replaces the default overflow glyph. */ icon?: ReactNode; /** Where the panel opens relative to the button. Defaults to below it. */ placement?: MenuContentProps['placement']; /** How it lines up on that edge. Defaults to the button's trailing edge. */ align?: MenuContentProps['align']; /** * Floor for the menu's width. A panel sized to its contents takes its width * from whatever inside it is not flexible — in a row of a flexible label and * a fixed glyph, that is the glyph, and the menu comes up as a column of * icons with the words squeezed out of it. */ minWidth?: number; /** Passed through to the panel — `width`, `maxHeight`, `offset` and the rest. */ contentProps?: Omit; /** The rows: `Menu.Item`, `Menu.Separator`, `Menu.Label`. */ children?: ReactNode; } /** * A row's actions, behind an overflow button at the end of it. * * ```tsx * * * Rename * * Delete * * * ``` * * The panel is anchored to the button rather than presented from the bottom of * the screen, so it lines up with the row it belongs to and the list it came * from stays readable behind it. It is also narrow: the panel it opens in is a * fraction of the screen, and a sheet covering that to offer four verbs costs * more than it says. * * Pressing the button does not press the row. A row that navigates would * otherwise navigate away from the thing the menu is about. */ declare function PanelsideItemActions({ className, label, icon, placement, align, minWidth, contentProps, children, }: PanelsideItemActionsProps): import("react").JSX.Element; declare namespace PanelsideItemActions { var displayName: string; } export interface PanelsideFooterProps extends ViewProps { className?: string; /** * Overlay the scrolling list instead of taking a row below it. Default true — * the list runs the full height of the panel behind it, and `Panelside.Content` * leaves exactly this footer's height of room at the end. */ floating?: boolean; /** * What the footer paints behind its controls. * * `transparent` is the default and paints nothing: the list runs under the * controls, which is how the panel reads as one surface with two things * floating on it rather than as a list with a bar bolted to the bottom. * * `fade` dissolves the list into the panel background over the strip above * the controls. It costs a band of the panel, and buys a compose button that * never has a chat title running through its label — worth turning on for a * panel whose history is long enough that something is always underneath. * * `solid` is a band with a hairline over it, for a footer that is a row of * the layout. Implied by `floating={false}`, which has no list to float over. */ surface?: PanelsideSurface; children?: ReactNode; } declare function PanelsideFooter({ className, floating, surface, children, style, ...props }: PanelsideFooterProps): import("react").JSX.Element; declare namespace PanelsideFooter { var displayName: string; } export interface PanelsideCtaProps extends Omit { className?: string; /** The button's text. */ label?: string; /** Leading element, usually an icon. */ icon?: ReactNode; /** `primary` is the filled accent pill; `secondary` is the quiet one. */ variant?: 'primary' | 'secondary'; /** * How tall the pill is. `default` is 44pt — a step above the account button * beside it, so the footer reads as one primary control and one secondary * one. `lg` is 52pt, for a panel where the call to action is the only thing * in the row, and `xl` is 56pt. * * Ignored under `native` — the platform sizes its own button, and asks for a * control size rather than a height. The three steps reach the platform's * regular, large and extra-large controls. */ size?: PanelsideCtaSize; /** * Render the platform's own button instead of the pill. Requires the * optional `@expo/ui` package; without it this prop does nothing. * * **Theme tokens do not apply** — the platform draws the button, so * `className` and `icon` are ignored and it sizes itself to `label`. */ native?: boolean; /** * Draw the native button in the platform's Liquid Glass material. Requires * `native`, and iOS 26 or later; ignored anywhere else. */ glass?: boolean; children?: ReactNode; } declare function PanelsideCta({ className, label, icon, variant, size, native, glass, disabled, children, ...props }: PanelsideCtaProps): import("react").JSX.Element; declare namespace PanelsideCta { var displayName: string; } export interface PanelsideSceneProps extends ViewProps { className?: string; /** * How small the scene gets at full travel. Default 1 — the screen keeps its * full height and stays behind the status bar, and the radius and dim do the * work. Below one it shrinks about its centre, which insets it top and bottom * as well as at the side. * * Falls back to the same prop on the `Panelside` root, so the three numbers * that describe the curve can be set once where the panel is configured. */ scale?: number; /** The corner radius the scene reaches at full travel. Default 44. */ radius?: number; /** How far the scene dims at full travel, 0 to 1. Default 0.45. */ dim?: number; /** * Styles the layer that dims the scene. Its opacity is `dim`'s to set, so * this is for the colour — a scrim that is not black, for a light theme * where black at 45% reads as a hole rather than as shade. */ scrimClassName?: string; children?: ReactNode; } declare function PanelsideScene({ className, scale: scaleProp, radius: radiusProp, dim: dimProp, scrimClassName, children, style, ...props }: PanelsideSceneProps): import("react").JSX.Element; declare namespace PanelsideScene { var displayName: string; } export interface PanelsidePagesProps extends ViewProps { className?: string; /** `Panelside.Page` elements. Anything else is rendered as given. */ children?: ReactNode; } /** * The pages the panel navigates between. * * Put it inside `Panelside.Scene` and give each page a `value` that a row's * `to` matches. Nothing is wired between the two: the row sets the panel's * route, and the page whose value equals it is the one shown. * * ```tsx * * * * * * * ``` * * A page is mounted the first time it is visited and stays mounted after * that, hidden rather than removed. Going back to one is then a style change * rather than a mount: its list does not rebuild, its scroll position is where * you left it, and whatever it was fetching is already there. A page whose * contents go stale — or whose data is large enough that keeping it is worse * than fetching it again — takes `keepAlive={false}`. */ declare function PanelsidePages({ className, children, ...props }: PanelsidePagesProps): import("react").JSX.Element; declare namespace PanelsidePages { var displayName: string; } export interface PanelsidePageProps extends ViewProps { className?: string; /** What a row's `to` has to equal for this page to be the one shown. */ value: string; /** * Keep the page mounted once it has been visited. Default true, which is * what makes going back to it instant. Off, it is torn down on the way out * and rebuilt on the way in. */ keepAlive?: boolean; /** * Set by `Panelside.Pages`. A hidden page is laid out by nobody, is not in * the accessibility tree, and takes no touches — but it is still mounted, * which is the whole point of it. */ hidden?: boolean; children?: ReactNode; } /** One page. Only meaningful inside `Panelside.Pages`. */ declare function PanelsidePage({ className, value, keepAlive, hidden, children, style, ...props }: PanelsidePageProps): import("react").JSX.Element; declare namespace PanelsidePage { var displayName: string; } export interface PanelsideTriggerProps extends Omit { className?: string; /** What a screen reader announces. */ label?: string; /** * A single pressable element to use instead of the default button. Its own * `onPress` still runs. */ children?: ReactElement<{ onPress?: (...args: unknown[]) => void; }>; } declare function PanelsideTrigger({ className, label, children, onPress, ...props }: PanelsideTriggerProps): import("react").JSX.Element | null; declare namespace PanelsideTrigger { var displayName: string; } export interface PanelsideSearchTriggerProps extends Omit { className?: string; /** What a screen reader announces. */ label?: string; /** * `filled` is the default: a circle in the secondary surface, which is what * a control sitting alone on the panel's own surface needs to read as one. * * `outline` is a ring and no fill, for a panel whose other controls are * outlined too — a filled circle among them is the only thing on the screen * claiming to be a second primary. * * Ignored under `native`, where the platform owns the button's chrome. */ variant?: PanelsideControlVariant; /** Replaces the default magnifier. */ children?: ReactNode; /** * Render the platform's own button instead of the circle. Requires the * optional `@expo/ui` package; without it this prop does nothing. */ native?: boolean; /** * Draw the native button in the platform's Liquid Glass material. Requires * `native`, and iOS 26 or later; ignored anywhere else. */ glass?: boolean; } /** * The search button. Goes in `Panelside.Header`'s `action` slot. * * It draws the control and nothing else: a circle, or the platform's own * button under `native`, with the magnifier already in it. What pressing it * opens is `onPress`'s to decide — a page the scene navigates to, a sheet, a * screen pushed onto the app's own stack. * * That is the same arrangement `Panelside.Trigger` has, and it is here for the * same reason: where search goes is a decision about the app, and a component * that answers it for you is one you have to work around the first time the * answer is different. */ declare function PanelsideSearchTrigger({ className, label, variant, children, native, glass, onPress, ...props }: PanelsideSearchTriggerProps): import("react").JSX.Element; declare namespace PanelsideSearchTrigger { var displayName: string; } export declare const Panelside: typeof PanelsideRoot & { Panel: typeof PanelsidePanel; Header: typeof PanelsideHeader; Search: typeof PanelsideSearch; Content: typeof PanelsideContent; Group: typeof PanelsideGroup; GroupLabel: typeof PanelsideGroupLabel; Item: typeof PanelsideItem; ItemIcon: typeof PanelsideItemIcon; ItemLabel: typeof PanelsideItemLabel; ItemBadge: typeof PanelsideItemBadge; Action: typeof PanelsideAction; ItemActions: typeof PanelsideItemActions; Footer: typeof PanelsideFooter; Cta: typeof PanelsideCta; Scene: typeof PanelsideScene; Pages: typeof PanelsidePages; Page: typeof PanelsidePage; Trigger: typeof PanelsideTrigger; SearchTrigger: typeof PanelsideSearchTrigger; }; export {}; //# sourceMappingURL=index.d.ts.map