/**
* 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