/**
* SearchBar — a text field for querying a list, with the two controls a search
* needs and an ordinary field does not, and a panel of results that opens out
* of the field itself.
*
* ```tsx
*
*
*
*
* } onPress={add}>Claude
*
*
* ```
*
* ## The results are above the field, and the field is above the keyboard
*
* A search that is being typed into has a keyboard under it, and a list drawn
* below the field is a list drawn behind the keyboard. So `avoidKeyboard`
* lifts the field until it sits `keyboardOffset` points clear of the keyboard's
* top edge, and the panel opens *upward* out of it into the space that is
* actually free.
*
* That puts the first result nearest the field and the last one furthest away,
* which is the order a reader walking away from the caret expects. Pass
* `panelPlacement="bottom"` for a search bar in a header, where the space is
* the other way round.
*
* The panel is positioned absolutely rather than laid out in the flow, so
* opening it never moves the page underneath — a list that pushes the field it
* belongs to is a field that walks away from the finger typing into it.
*
* ## Touches inside the panel must not close the keyboard
*
* The panel scrolls with `keyboardShouldPersistTaps="always"`, and every press
* inside it holds the field's focus open for a moment afterwards. Both are
* needed, because a search closes the instant the field blurs and there are
* two separate ways for a touch in the panel to blur it.
*
* `"handled"` only spares presses a child takes responsibility for, which
* leaves the panel's own padding, the gaps between rows, a section heading and
* the whole of `SearchBar.Status` as live dismiss surfaces — tapping the word
* "Searching …" would end the search. `"always"` gives the panel back.
*
* The focus guard covers the other way: a control inside a row — an add
* button, a remove ✕ — takes focus with the press on Android, and returning it
* a frame later is not enough on its own, because the blur has already closed
* the panel the control was drawn in. So a press in the panel marks the field
* as still being used, and a blur arriving under that mark is answered by
* asking for focus back rather than by ending the search.
*
* That guard only knows about presses that go through this component's own
* parts, and a caller's `Pressable` in a row's `trailing` slot takes the touch
* itself. So the panel also waits before believing any blur, and asks the
* keyboard: it is still up, because nothing in the panel dismisses it, and a
* search whose keyboard is still up has not ended. Focus goes back instead.
*
* ## The space kept for the field is not a target
*
* The card is one box around the results *and* the field, so it carries a
* spacer where the field sits. That spacer is a plain view drawn over a
* focused field, and a touch on a plain view is the platform's cue to dismiss
* the keyboard — so winning one blurred the field and closed the panel drawn
* out of that focus. Tapping the search box shut the results, which is exactly
* backwards. The card and its spacer take no touches at all now.
*
* ## What has already been picked goes in the field
*
* `tokens` puts the choices made so far inside the field, before the caret, so
* the query and what it has produced are one control rather than a control and
* a list somewhere above it. `SearchBar.Token` is the chip; backspace on an
* empty field fires `onRemoveLastToken`, which is what a token field does
* everywhere else.
*
* They scroll rather than wrap: the field is one line tall, and a row of chips
* that grew it would move the caret every time something was picked.
*
* ## The clear button, and why it is not the platform's
*
* A ✕ appears inside the field as soon as there is something to clear, and
* takes it back to empty without dismissing the keyboard — clearing a query is
* the start of the next one, not the end of the search. It is drawn here
* rather than left to `clearButtonMode`, which exists on iOS only, cannot be
* labelled for a screen reader and cannot be swapped for a spinner while
* results are in flight.
*
* The glyph is 24 points and its touch box is 48, made up with slop rather
* than with size. A 48-point circle inside a 40-point field either overflows
* it or forces every search bar in an app to be as tall as the largest one.
*
* ## Cancel is a row, not a decoration
*
* `cancel="focus"` puts a Cancel button beside the field and slides it in
* while the field is being edited, which is the platform's own answer to
* "how do I get out of this search". It is a sibling of the field rather than
* something inside it, because it acts on the search as a whole: it empties
* the query, drops focus and calls `onCancel`, and a control that ends the
* thing it sits inside reads as part of the query it is about to discard.
*
* Its width is measured once and animated on the UI thread. The button is
* always mounted when `cancel` is not `never`, so the measurement is already
* there the first time the field is touched and the first slide is as smooth
* as the tenth.
*
* ## Debouncing belongs to the caller's search, not to the field
*
* `onChangeText` always fires on every keystroke — a controlled field that
* lags its own input is unusable. `debounce` is about the *query*: it holds
* `onDebouncedChange` until typing pauses, so a network search runs once per
* pause instead of once per letter. Submitting flushes it immediately, since
* a return key is somebody saying they are done waiting.
*/
import { type ReactNode } from 'react';
import { TextInput, type ViewProps } from 'react-native';
import { type VariantProps } from 'tailwind-variants';
import { type AnimatedPressableProps } from '../../primitives/animated-pressable.js';
import { type InputProps } from '../input/index.js';
declare const searchBarVariants: import("tailwind-variants").TVReturnType<{
size: {
sm: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
md: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
lg: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
};
/**
* The field's corner. `pill` is the shape a search field takes when it is
* chrome — sitting above a list, in a header — and `rounded` the one it
* takes inside a form beside other fields.
*/
shape: {
rounded: {
field: string;
};
pill: {
field: string;
};
};
/**
* Which edge of the field the card grows out of. The field's corners on
* that edge go square and its border comes off entirely — the card around
* both of them is what draws the edge.
*/
attached: {
none: {};
top: {
field: string;
};
bottom: {
field: string;
};
};
selected: {
true: {
item: string;
};
};
}, {
row: string;
anchor: string;
field: string;
panel: string;
panelList: string;
/** The hairline between the results and the field. */
panelDivider: string;
section: string;
sectionLabel: string;
item: string;
itemLabel: string;
status: string;
cancelClip: string;
cancelButton: string;
cancelLabel: string;
clear: string;
tokenRow: string;
token: string;
tokenLabel: string;
tokenRemove: string;
}, undefined, {
size: {
sm: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
md: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
lg: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
};
/**
* The field's corner. `pill` is the shape a search field takes when it is
* chrome — sitting above a list, in a header — and `rounded` the one it
* takes inside a form beside other fields.
*/
shape: {
rounded: {
field: string;
};
pill: {
field: string;
};
};
/**
* Which edge of the field the card grows out of. The field's corners on
* that edge go square and its border comes off entirely — the card around
* both of them is what draws the edge.
*/
attached: {
none: {};
top: {
field: string;
};
bottom: {
field: string;
};
};
selected: {
true: {
item: string;
};
};
}, {
row: string;
anchor: string;
field: string;
panel: string;
panelList: string;
/** The hairline between the results and the field. */
panelDivider: string;
section: string;
sectionLabel: string;
item: string;
itemLabel: string;
status: string;
cancelClip: string;
cancelButton: string;
cancelLabel: string;
clear: string;
tokenRow: string;
token: string;
tokenLabel: string;
tokenRemove: string;
}, import("tailwind-variants").TVReturnType<{
size: {
sm: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
md: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
lg: {
cancelLabel: string;
clear: string;
token: string;
tokenLabel: string;
tokenRemove: string;
};
};
/**
* The field's corner. `pill` is the shape a search field takes when it is
* chrome — sitting above a list, in a header — and `rounded` the one it
* takes inside a form beside other fields.
*/
shape: {
rounded: {
field: string;
};
pill: {
field: string;
};
};
/**
* Which edge of the field the card grows out of. The field's corners on
* that edge go square and its border comes off entirely — the card around
* both of them is what draws the edge.
*/
attached: {
none: {};
top: {
field: string;
};
bottom: {
field: string;
};
};
selected: {
true: {
item: string;
};
};
}, {
row: string;
anchor: string;
field: string;
panel: string;
panelList: string;
/** The hairline between the results and the field. */
panelDivider: string;
section: string;
sectionLabel: string;
item: string;
itemLabel: string;
status: string;
cancelClip: string;
cancelButton: string;
cancelLabel: string;
clear: string;
tokenRow: string;
token: string;
tokenLabel: string;
tokenRemove: string;
}, undefined, unknown, unknown, undefined>>;
type SearchBarVariantProps = VariantProps;
/** Where the results open. */
export type SearchBarPanelPlacement = 'top' | 'bottom';
/** When the results are shown. */
export type SearchBarPanelMode = 'never' | 'focus' | 'always';
/**
* What SearchBar takes from Input, minus everything it owns itself. The form
* furniture is dropped along with it: a label and an error line stack above
* and below the field, and Cancel sits beside the whole stack rather than
* beside the field it belongs to. Use `Field` for a search that is one answer
* in a form.
*
* The keyboard props go too. Input's would move the field and leave the Cancel
* button and the panel where they were; SearchBar lifts all three together.
*/
type InheritedInputProps = Omit;
export interface SearchBarProps extends InheritedInputProps, Omit {
/**
* The field's background, from `Input`. `outline` draws its own edge, for a
* search bar sitting on the page; `filled` drops it, for one inside a card
* or a header where a second border reads as a seam. Defaults to `outline`.
*/
variant?: InputProps['variant'];
/** The query, when the caller holds it. Leave unset to let the field keep it. */
value?: string;
/** Starting query for an uncontrolled field. Ignored once `value` is passed. */
defaultValue?: string;
/** Fires on every keystroke. For a search that costs something, see `debounce`. */
onChangeText?: (value: string) => void;
/** The return key, which is labelled Search. Flushes `onDebouncedChange` first. */
onSubmit?: (value: string) => void;
/**
* How long typing has to pause before `onDebouncedChange` runs, in
* milliseconds. `0` runs it on every keystroke, which is only right for a
* filter over a list already in memory.
*/
debounce?: number;
/** The query, once typing has paused for `debounce` milliseconds. */
onDebouncedChange?: (value: string) => void;
/** Fires after the ✕ empties the field. The field keeps focus. */
onClear?: () => void;
/** Fires after Cancel empties the field and drops focus. */
onCancel?: () => void;
/** Whether the ✕ appears once there is a query. */
isClearable?: boolean;
/**
* When the Cancel button is beside the field. `focus` slides it in while the
* field is being edited and away again when it is not, which is what a
* search bar above a list wants. `always` keeps it out, for a screen that is
* nothing but the search.
*/
cancel?: 'never' | 'focus' | 'always';
/** The Cancel button's word. */
cancelLabel?: string;
/** How the ✕ announces itself. */
clearLabel?: string;
/**
* Results are on their way. A spinner takes the ✕'s place, because the two
* would otherwise sit on top of one another at exactly the moment a query is
* both non-empty and running.
*/
loading?: boolean;
/** The leading glyph, for a search over something with a symbol of its own. */
icon?: ReactNode;
/**
* Lift the whole search — field, Cancel button and panel — until it sits
* clear of the software keyboard, and put it back on blur. Without it the
* field stays where the page left it, which on most screens is behind the
* keyboard it just opened.
*
* Install `react-native-keyboard-controller` for this to behave on Android.
*
* Do not toggle it at runtime: it changes which component wraps the row, so
* the field would remount and lose focus.
*/
avoidKeyboard?: boolean;
/** Gap kept between the field's bottom edge and the keyboard. */
keyboardOffset?: number;
/**
* When the results panel is shown. `focus` opens it while the field is being
* typed into, `always` keeps it out for a screen that is nothing but the
* search, `never` ignores the children entirely.
*/
panel?: SearchBarPanelMode;
/**
* Which side of the field the panel opens out of. `top` is the default,
* because the space under a focused field belongs to the keyboard.
*/
panelPlacement?: SearchBarPanelPlacement;
/**
* Cap on the panel's height, in points.
*
* The panel takes the smaller of this and the room between the field and the
* edge of the screen, so it never runs off the top of the display. Unset, it
* is capped at about six rows: the space above a lifted field is most of the
* screen, and a panel that takes all of it stops reading as something laid
* over the app. Longer lists scroll.
*/
panelMaxHeight?: number;
/**
* What has been picked so far, drawn inside the field before the caret.
* `SearchBar.Token` is the chip; anything else that fits on one line works
* too. Tokens scroll rather than wrap, so the field stays one line tall.
*/
tokens?: ReactNode;
/**
* Fires when backspace is pressed in an empty field. Remove the last token
* here — it is the gesture every token field answers, and without it the
* only way back out of a choice is its own ✕.
*/
onRemoveLastToken?: () => void;
/** The panel's contents — `SearchBar.Section`, `.Item` and `.Status`. */
children?: ReactNode;
}
export interface SearchBarSectionProps extends ViewProps {
className?: string;
/**
* The heading over the run of rows — "Suggested", "Results". Announced as a
* header, so a screen reader reaching the group is told what it is before
* walking into it.
*/
label?: string;
children?: ReactNode;
}
/** A labelled run of rows inside the panel. */
declare function SearchBarSection({ className, label, children, ...props }: SearchBarSectionProps): import("react").JSX.Element;
declare namespace SearchBarSection {
var displayName: string;
}
export interface SearchBarItemProps extends Omit {
className?: string;
/** Anything before the label — an avatar, a logo, a status dot. */
leading?: ReactNode;
/**
* Anything after it. A slot rather than a built-in button, because what a
* result row offers differs per search: an add, a pin, a count, nothing.
*/
trailing?: ReactNode;
/** A second line under the label, for what the label alone cannot say. */
description?: string;
/** Draws the row as the one the search has settled on. */
selected?: boolean;
/** The row's label. */
children?: ReactNode;
}
/** One result. */
declare function SearchBarItem({ className, leading, trailing, description, selected, children, onPressIn, ...props }: SearchBarItemProps): import("react").JSX.Element;
declare namespace SearchBarItem {
var displayName: string;
}
export interface SearchBarStatusProps extends ViewProps {
className?: string;
/** A spinner beside the line, for a search that is still running. */
loading?: boolean;
children?: ReactNode;
}
/**
* The one line a panel shows instead of rows — nothing typed yet, a search in
* flight, or a query that matched nothing. It is a sentence rather than an
* empty box because those three states look identical when they are blank, and
* which one it is decides what the person does next.
*/
declare function SearchBarStatus({ className, loading, children, ...props }: SearchBarStatusProps): import("react").JSX.Element;
declare namespace SearchBarStatus {
var displayName: string;
}
export interface SearchBarActionProps extends AnimatedPressableProps {
className?: string;
children?: ReactNode;
}
/**
* A button inside a row — an add, a pin, a remove — for the `trailing` slot.
*
* It exists rather than being left to a plain `Pressable` because a control
* nested inside a row takes the touch itself, so the row above it never sees
* the press and cannot hold the field's focus on its behalf. Pressed, this one
* ends up blurring the field, and a blurred field closes the panel the button
* was standing in — the press lands and the search disappears under it.
*/
declare function SearchBarAction({ className, children, onPressIn, ...props }: SearchBarActionProps): import("react").JSX.Element;
declare namespace SearchBarAction {
var displayName: string;
}
export interface SearchBarTokenProps extends ViewProps {
className?: string;
/** Anything before the label — an avatar, a logo, a status dot. */
leading?: ReactNode;
/** Fires when the chip's ✕ is pressed. Without it no ✕ is drawn. */
onRemove?: () => void;
/** How the ✕ announces itself. Defaults to `Remove