Pinned search field, recent-search rows with per-item remove, trending chips, suggested rows, and a results state with empty state
import * as React from "react";
import { useEffect, useState } from "react";
import { View, FlatList, ScrollView, Pressable } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Button, Chip, EmptyState, Icon, Image, SearchInput, Text, type IconName } from "@lumioui/ui";
import { AnimatedPressable, cn, useHaptic, usePressAnimation, type ColorTokens } from "@lumioui/core";
import { Mount, Press, ROW_STAGGER, ROW_STAGGER_CAP, StoryScrim } from "../shared";
export interface SearchResult {
id: string;
title: string;
subtitle?: string;
imageUrl?: string;
/** Leading icon when there's no thumbnail (e.g. "grid", "user"). */
icon?: IconName;
}
export interface SearchScreenProps {
/** Results for the current query. When omitted, a small built-in dataset is
* filtered by the query so the template feels alive without a backend. */
results?: SearchResult[];
recentSearches?: string[];
trendingSearches?: string[];
onSearch?: (query: string) => void;
onResultPress?: (result: SearchResult) => void;
onRecentPress?: (query: string) => void;
onTrendingPress?: (query: string) => void;
onRemoveRecent?: (query: string) => void;
onClearRecent?: () => void;
}
const img = (seed: string) => `https://picsum.photos/seed/${seed}/200/200`;
const face = (n: number) => `https://i.pravatar.cc/150?img=${n}`;
const DEFAULT_RECENT = ["design tokens", "animated charts", "haptic feedback", "onboarding flow"];
const DEFAULT_TRENDING = ["Reanimated 4", "NativeWind", "Liquid glass", "Expo Router", "FlashList"];
/** Per-chip tint rotation for trending — each topic reads as its own
* "channel" instead of five identical pills. Semantic tokens only. */
const TRENDING_TONES: { bg: string; text: string; icon: keyof ColorTokens }[] = [
{ bg: "bg-primary/10", text: "text-primary", icon: "primary" },
{ bg: "bg-chart-2/15", text: "text-chart-2", icon: "chart2" },
{ bg: "bg-chart-3/15", text: "text-chart-3", icon: "chart3" },
{ bg: "bg-chart-5/15", text: "text-chart-5", icon: "chart5" },
{ bg: "bg-warning/15", text: "text-warning", icon: "warning" },
];
/** Idle-state suggestions — rendered as the same result rows a query
* produces, the App Store "Suggested" idiom. */
const DEFAULT_SUGGESTED: SearchResult[] = [
{
id: "s1",
title: "Bottom sheet",
subtitle: "Pro component · Overlays",
imageUrl: img("lumioui-sug-1"),
},
{
id: "s2",
title: "Coach marks",
subtitle: "Pro component · Onboarding",
imageUrl: img("lumioui-sug-2"),
},
{
id: "s3",
title: "Paywall sheet",
subtitle: "Pro component · Monetization",
imageUrl: img("lumioui-sug-3"),
},
];
const DEFAULT_RESULTS: SearchResult[] = [
{
id: "r1",
title: "Animated charts",
subtitle: "Pro component · Charts",
imageUrl: img("lumioui-search-1"),
},
{
id: "r2",
title: "Haptic feedback",
subtitle: "Guide · Best practices",
imageUrl: img("lumioui-search-2"),
},
{ id: "r3", title: "Design tokens", subtitle: "Guide · Theming", icon: "book-open" },
{
id: "r4",
title: "Onboarding flow",
subtitle: "Screen template · Auth",
imageUrl: img("lumioui-search-4"),
},
{ id: "r5", title: "Maya Chen", subtitle: "Product designer · 128 posts", imageUrl: face(47) },
];
interface SearchResultRowProps {
item: SearchResult;
onPress?: (item: SearchResult) => void;
}
function SearchResultRow({ item, onPress }: SearchResultRowProps) {
const haptics = useHaptic();
const press = usePressAnimation({ fast: true });
return (
<AnimatedPressable
onPress={() => {
haptics.light();
onPress?.(item);
}}
onPressIn={press.onPressIn}
onPressOut={press.onPressOut}
style={press.animatedStyle}
accessibilityRole="button"
accessibilityLabel={item.subtitle ? `${item.title}, ${item.subtitle}` : item.title}
className="min-h-11 flex-row items-center gap-3 py-2"
>
{item.imageUrl ? (
<Image
source={{ uri: item.imageUrl }}
rounded="xl"
className="h-11 w-11"
accessibilityLabel={item.title}
/>
) : (
<View className="h-11 w-11 items-center justify-center rounded-xl bg-[#F4F5F9] dark:bg-white/10">
<Icon name={item.icon ?? "search"} size="sm" color="mutedForeground" />
</View>
)}
<View className="flex-1 gap-0.5">
<Text variant="small" className="font-semibold" numberOfLines={1}>
{item.title}
</Text>
{item.subtitle ? (
<Text variant="muted" numberOfLines={1}>
{item.subtitle}
</Text>
) : null}
</View>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</AnimatedPressable>
);
}
SearchResultRow.displayName = "SearchResultRow";
/** Section label over the idle screen — muted uppercase microcopy built
* from explicit tokens on top of the muted variant. */
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<Text
variant="muted"
accessibilityRole="header"
className={cn("text-xs font-semibold uppercase tracking-wider")}
>
{children}
</Text>
);
}
SectionLabel.displayName = "SectionLabel";
interface RecentRowProps {
term: string;
onPress: (term: string) => void;
onRemove: (term: string) => void;
}
/** Recent-search row — clock glyph, term, per-item remove. */
function RecentRow({ term, onPress, onRemove }: RecentRowProps) {
const haptics = useHaptic();
const press = usePressAnimation({ fast: true });
return (
<AnimatedPressable
onPress={() => {
haptics.light();
onPress(term);
}}
onPressIn={press.onPressIn}
onPressOut={press.onPressOut}
style={press.animatedStyle}
accessibilityRole="button"
accessibilityLabel={`Search for ${term}`}
className="min-h-11 flex-row items-center gap-3 py-1.5"
>
<Icon name="clock" size="sm" color="mutedForeground" />
<Text variant="small" className="flex-1" numberOfLines={1}>
{term}
</Text>
<Pressable
onPress={() => {
haptics.light();
onRemove(term);
}}
accessibilityRole="button"
accessibilityLabel={`Remove ${term} from recent searches`}
hitSlop={10}
className="h-8 w-8 items-center justify-center"
>
<Icon name="x" size="sm" color="mutedForeground" />
</Pressable>
</AnimatedPressable>
);
}
RecentRow.displayName = "RecentRow";
/** Search — pinned autofocus field, recent rows, trending chips, results. */
export function SearchScreen({
results,
recentSearches = DEFAULT_RECENT,
trendingSearches = DEFAULT_TRENDING,
onSearch,
onResultPress,
onRecentPress,
onTrendingPress,
onRemoveRecent,
onClearRecent,
}: SearchScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [query, setQuery] = useState("");
// Local mirror of recents so remove/clear feel alive when the consumer
// hasn't wired state yet; prop changes re-sync.
const [recents, setRecents] = useState(recentSearches);
useEffect(() => setRecents(recentSearches), [recentSearches]);
const updateQuery = (text: string) => {
setQuery(text);
onSearch?.(text);
};
const runSearch = (term: string) => {
updateQuery(term);
};
const removeRecent = (term: string) => {
setRecents((prev) => prev.filter((t) => t !== term));
onRemoveRecent?.(term);
};
const clearRecents = () => {
setRecents([]);
onClearRecent?.();
};
const trimmed = query.trim();
const searching = trimmed.length > 0;
const lower = trimmed.toLowerCase();
const activeResults =
results ??
DEFAULT_RESULTS.filter((r) =>
`${r.title} ${r.subtitle ?? ""}`.toLowerCase().includes(lower)
);
return (
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
{/* Pinned header — the field never scrolls away. */}
<Mount>
<View className="gap-3 px-5 pb-3 pt-4">
<Text variant="h2" accessibilityRole="header">
Search
</Text>
<SearchInput
glass
placeholder="Components, guides, people…"
value={query}
onChangeText={updateQuery}
onClear={() => updateQuery("")}
// autoFocus removed to prevent auto-scrolling embedded previews
returnKeyType="search"
accessibilityLabel="Search"
/>
{/* Query hint — real searches teach the grammar once. */}
<Text variant="muted" className="pt-2 text-xs">
Try "glass" — or filter with tag:motion
</Text>
</View>
</Mount>
{!searching ? (
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
contentContainerStyle={{ paddingBottom: insets.bottom + 32 }}
>
{/* Featured — a photo-led discovery card so the idle state opens
with something worth tapping, not a list of chores. */}
<Mount>
<Press
haptic="light"
onPress={() => runSearch("bottom sheet")}
accessibilityRole="button"
accessibilityLabel="Featured: Bottom sheet, new this week"
className="mx-6 mt-1 h-36 overflow-hidden rounded-3xl p-0"
>
<Image
source={{ uri: "https://picsum.photos/seed/lumioui-featured/560/288" }}
className="absolute inset-0 h-full w-full"
accessibilityElementsHidden
/>
<View className="absolute inset-x-0 bottom-0 h-20">
<StoryScrim edge="bottom" />
</View>
<View className="absolute left-3 top-3 rounded-full bg-[#BEF04C] px-2.5 py-1">
<Text className="text-[11px] font-semibold text-[#1B1B26]">
Featured this week
</Text>
</View>
<View className="absolute inset-x-0 bottom-0 p-4">
<Text className="text-base font-semibold text-white">Bottom sheet</Text>
<Text className="text-xs text-white/75">
Pro component · inertia dismissal, keyboard-aware
</Text>
</View>
</Press>
</Mount>
{/* Recent searches */}
<Mount delay={50}>
<View className="flex-row items-center justify-between px-6 pt-3">
<SectionLabel>Recent</SectionLabel>
{recents.length > 0 ? (
<Pressable
onPress={() => {
haptics.light();
clearRecents();
}}
accessibilityRole="button"
accessibilityLabel="Clear all recent searches"
hitSlop={8}
className="min-h-11 justify-center"
>
<Text variant="small" className="text-primary">
Clear all
</Text>
</Pressable>
) : null}
</View>
<View className="px-6 pt-1">
{recents.map((term) => (
<RecentRow
key={term}
term={term}
onPress={(t) => {
runSearch(t);
onRecentPress?.(t);
}}
onRemove={removeRecent}
/>
))}
{recents.length === 0 ? (
<Text variant="muted" className="py-2">
No recent searches.
</Text>
) : null}
</View>
</Mount>
{/* Trending */}
{trendingSearches.length > 0 ? (
<Mount delay={110}>
<View className="px-6 pt-6">
<SectionLabel>Trending</SectionLabel>
<View className="flex-row flex-wrap gap-2 pt-3">
{trendingSearches.map((term, i) => {
const tone = TRENDING_TONES[i % TRENDING_TONES.length] ?? TRENDING_TONES[0]!;
return (
<Chip
key={term}
variant="suggestion"
icon={<Icon name="flame" size="sm" color={tone.icon} />}
onPress={() => {
runSearch(term);
onTrendingPress?.(term);
}}
accessibilityLabel={`Search trending topic ${term}`}
className={cn("min-h-11 px-4", tone.bg)}
textClassName={tone.text}
>
{term}
</Chip>
);
})}
</View>
</View>
</Mount>
) : null}
{/* Suggested — result-style rows so the idle screen still ends
with content, the way App Store search does. */}
<Mount delay={170}>
<View className="px-6 pt-6">
<SectionLabel>Suggested</SectionLabel>
<View className="pt-2">
{DEFAULT_SUGGESTED.map((item, i) => (
<View key={item.id}>
<SearchResultRow item={item} onPress={onResultPress} />
{i < DEFAULT_SUGGESTED.length - 1 ? (
<View className="ml-[56px] h-px bg-border/60" />
) : null}
</View>
))}
</View>
</View>
</Mount>
</ScrollView>
) : (
<FlatList
data={activeResults}
keyExtractor={(item) => item.id}
keyboardShouldPersistTaps="handled"
contentContainerClassName="gap-1 px-6 pt-2"
contentContainerStyle={{ paddingBottom: insets.bottom + 32 }}
ListEmptyComponent={
<EmptyState
variant="search"
title="No results"
description={`Nothing matched “${trimmed}”. Try a different term.`}
action={
<Button size="sm" variant="secondary" onPress={() => updateQuery("")}>
Clear search
</Button>
}
className="py-20"
/>
}
renderItem={({ item, index }) => (
<Mount delay={Math.min(index * ROW_STAGGER, ROW_STAGGER_CAP)}>
<SearchResultRow item={item} onPress={onResultPress} />
</Mount>
)}
/>
)}
</SafeAreaView>
);
}
Recreate the "Search Screen" screen in React Native using LumioUI.
Goal: Pinned search field, recent-search rows with per-item remove, trending chips, suggested rows, and a results state with empty state Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.
## Setup
Fastest path — install the screen plus its component deps:
```bash
npx lumioui add core-search
```
Or pull just the components: `npx lumioui add button chip empty-state glass icon image search-input text`
Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*
Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.
## Components — how, where, when
### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×1
### `chip` — Compact selectable pill with press scale and ink overlay.
- `<Chip>` ×1
### `empty-state` — Illustrated placeholder with title, copy and action slot.
- `<EmptyState>` ×1
### `glass` — iOS 26 liquid-glass surface — real glass via expo-glass-effect (Expo Go) or @callstack/liquid-glass (dev builds), SVG refraction + specular rim on web, frosted fallback elsewhere; GlassContainer melts adjacent panes into one blob
- imported but not rendered directly — likely used through a sibling or as a utility
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×5
### `image` — Cached image with placeholder, fallback and aspect handling.
- `<Image>` ×2
### `search-input` — Search field with clear button, debounce, focus halo — plus a glass capsule mode that floats over content like iOS 26
- `<SearchInput>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×11
## Conventions (non-negotiable)
- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.
## Deliverable
One file: `screens/core/search.tsx` exporting SearchScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.
## Reference implementation
The LumioUI source for this screen — reproduce it faithfully:
```tsx
import * as React from "react";
import { useEffect, useState } from "react";
import { View, FlatList, ScrollView, Pressable } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Button, Chip, EmptyState, Icon, Image, SearchInput, Text, type IconName } from "@lumioui/ui";
import { AnimatedPressable, cn, useHaptic, usePressAnimation, type ColorTokens } from "@lumioui/core";
import { Mount, Press, ROW_STAGGER, ROW_STAGGER_CAP, StoryScrim } from "../shared";
export interface SearchResult {
id: string;
title: string;
subtitle?: string;
imageUrl?: string;
/** Leading icon when there's no thumbnail (e.g. "grid", "user"). */
icon?: IconName;
}
export interface SearchScreenProps {
/** Results for the current query. When omitted, a small built-in dataset is
* filtered by the query so the template feels alive without a backend. */
results?: SearchResult[];
recentSearches?: string[];
trendingSearches?: string[];
onSearch?: (query: string) => void;
onResultPress?: (result: SearchResult) => void;
onRecentPress?: (query: string) => void;
onTrendingPress?: (query: string) => void;
onRemoveRecent?: (query: string) => void;
onClearRecent?: () => void;
}
const img = (seed: string) => `https://picsum.photos/seed/${seed}/200/200`;
const face = (n: number) => `https://i.pravatar.cc/150?img=${n}`;
const DEFAULT_RECENT = ["design tokens", "animated charts", "haptic feedback", "onboarding flow"];
const DEFAULT_TRENDING = ["Reanimated 4", "NativeWind", "Liquid glass", "Expo Router", "FlashList"];
/** Per-chip tint rotation for trending — each topic reads as its own
* "channel" instead of five identical pills. Semantic tokens only. */
const TRENDING_TONES: { bg: string; text: string; icon: keyof ColorTokens }[] = [
{ bg: "bg-primary/10", text: "text-primary", icon: "primary" },
{ bg: "bg-chart-2/15", text: "text-chart-2", icon: "chart2" },
{ bg: "bg-chart-3/15", text: "text-chart-3", icon: "chart3" },
{ bg: "bg-chart-5/15", text: "text-chart-5", icon: "chart5" },
{ bg: "bg-warning/15", text: "text-warning", icon: "warning" },
];
/** Idle-state suggestions — rendered as the same result rows a query
* produces, the App Store "Suggested" idiom. */
const DEFAULT_SUGGESTED: SearchResult[] = [
{
id: "s1",
title: "Bottom sheet",
subtitle: "Pro component · Overlays",
imageUrl: img("lumioui-sug-1"),
},
{
id: "s2",
title: "Coach marks",
subtitle: "Pro component · Onboarding",
imageUrl: img("lumioui-sug-2"),
},
{
id: "s3",
title: "Paywall sheet",
subtitle: "Pro component · Monetization",
imageUrl: img("lumioui-sug-3"),
},
];
const DEFAULT_RESULTS: SearchResult[] = [
{
id: "r1",
title: "Animated charts",
subtitle: "Pro component · Charts",
imageUrl: img("lumioui-search-1"),
},
{
id: "r2",
title: "Haptic feedback",
subtitle: "Guide · Best practices",
imageUrl: img("lumioui-search-2"),
},
{ id: "r3", title: "Design tokens", subtitle: "Guide · Theming", icon: "book-open" },
{
id: "r4",
title: "Onboarding flow",
subtitle: "Screen template · Auth",
imageUrl: img("lumioui-search-4"),
},
{ id: "r5", title: "Maya Chen", subtitle: "Product designer · 128 posts", imageUrl: face(47) },
];
interface SearchResultRowProps {
item: SearchResult;
onPress?: (item: SearchResult) => void;
}
function SearchResultRow({ item, onPress }: SearchResultRowProps) {
const haptics = useHaptic();
const press = usePressAnimation({ fast: true });
return (
<AnimatedPressable
onPress={() => {
haptics.light();
onPress?.(item);
}}
onPressIn={press.onPressIn}
onPressOut={press.onPressOut}
style={press.animatedStyle}
accessibilityRole="button"
accessibilityLabel={item.subtitle ? `${item.title}, ${item.subtitle}` : item.title}
className="min-h-11 flex-row items-center gap-3 py-2"
>
{item.imageUrl ? (
<Image
source={{ uri: item.imageUrl }}
rounded="xl"
className="h-11 w-11"
accessibilityLabel={item.title}
/>
) : (
<View className="h-11 w-11 items-center justify-center rounded-xl bg-[#F4F5F9] dark:bg-white/10">
<Icon name={item.icon ?? "search"} size="sm" color="mutedForeground" />
</View>
)}
<View className="flex-1 gap-0.5">
<Text variant="small" className="font-semibold" numberOfLines={1}>
{item.title}
</Text>
{item.subtitle ? (
<Text variant="muted" numberOfLines={1}>
{item.subtitle}
</Text>
) : null}
</View>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</AnimatedPressable>
);
}
SearchResultRow.displayName = "SearchResultRow";
/** Section label over the idle screen — muted uppercase microcopy built
* from explicit tokens on top of the muted variant. */
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<Text
variant="muted"
accessibilityRole="header"
className={cn("text-xs font-semibold uppercase tracking-wider")}
>
{children}
</Text>
);
}
SectionLabel.displayName = "SectionLabel";
interface RecentRowProps {
term: string;
onPress: (term: string) => void;
onRemove: (term: string) => void;
}
/** Recent-search row — clock glyph, term, per-item remove. */
function RecentRow({ term, onPress, onRemove }: RecentRowProps) {
const haptics = useHaptic();
const press = usePressAnimation({ fast: true });
return (
<AnimatedPressable
onPress={() => {
haptics.light();
onPress(term);
}}
onPressIn={press.onPressIn}
onPressOut={press.onPressOut}
style={press.animatedStyle}
accessibilityRole="button"
accessibilityLabel={`Search for ${term}`}
className="min-h-11 flex-row items-center gap-3 py-1.5"
>
<Icon name="clock" size="sm" color="mutedForeground" />
<Text variant="small" className="flex-1" numberOfLines={1}>
{term}
</Text>
<Pressable
onPress={() => {
haptics.light();
onRemove(term);
}}
accessibilityRole="button"
accessibilityLabel={`Remove ${term} from recent searches`}
hitSlop={10}
className="h-8 w-8 items-center justify-center"
>
<Icon name="x" size="sm" color="mutedForeground" />
</Pressable>
</AnimatedPressable>
);
}
RecentRow.displayName = "RecentRow";
/** Search — pinned autofocus field, recent rows, trending chips, results. */
export function SearchScreen({
results,
recentSearches = DEFAULT_RECENT,
trendingSearches = DEFAULT_TRENDING,
onSearch,
onResultPress,
onRecentPress,
onTrendingPress,
onRemoveRecent,
onClearRecent,
}: SearchScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [query, setQuery] = useState("");
// Local mirror of recents so remove/clear feel alive when the consumer
// hasn't wired state yet; prop changes re-sync.
const [recents, setRecents] = useState(recentSearches);
useEffect(() => setRecents(recentSearches), [recentSearches]);
const updateQuery = (text: string) => {
setQuery(text);
onSearch?.(text);
};
const runSearch = (term: string) => {
updateQuery(term);
};
const removeRecent = (term: string) => {
setRecents((prev) => prev.filter((t) => t !== term));
onRemoveRecent?.(term);
};
const clearRecents = () => {
setRecents([]);
onClearRecent?.();
};
const trimmed = query.trim();
const searching = trimmed.length > 0;
const lower = trimmed.toLowerCase();
const activeResults =
results ??
DEFAULT_RESULTS.filter((r) =>
`${r.title} ${r.subtitle ?? ""}`.toLowerCase().includes(lower)
);
return (
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
{/* Pinned header — the field never scrolls away. */}
<Mount>
<View className="gap-3 px-5 pb-3 pt-4">
<Text variant="h2" accessibilityRole="header">
Search
</Text>
<SearchInput
glass
placeholder="Components, guides, people…"
value={query}
onChangeText={updateQuery}
onClear={() => updateQuery("")}
// autoFocus removed to prevent auto-scrolling embedded previews
returnKeyType="search"
accessibilityLabel="Search"
/>
{/* Query hint — real searches teach the grammar once. */}
<Text variant="muted" className="pt-2 text-xs">
Try "glass" — or filter with tag:motion
</Text>
</View>
</Mount>
{!searching ? (
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
contentContainerStyle={{ paddingBottom: insets.bottom + 32 }}
>
{/* Featured — a photo-led discovery card so the idle state opens
with something worth tapping, not a list of chores. */}
<Mount>
<Press
haptic="light"
onPress={() => runSearch("bottom sheet")}
accessibilityRole="button"
accessibilityLabel="Featured: Bottom sheet, new this week"
className="mx-6 mt-1 h-36 overflow-hidden rounded-3xl p-0"
>
<Image
source={{ uri: "https://picsum.photos/seed/lumioui-featured/560/288" }}
className="absolute inset-0 h-full w-full"
accessibilityElementsHidden
/>
<View className="absolute inset-x-0 bottom-0 h-20">
<StoryScrim edge="bottom" />
</View>
<View className="absolute left-3 top-3 rounded-full bg-[#BEF04C] px-2.5 py-1">
<Text className="text-[11px] font-semibold text-[#1B1B26]">
Featured this week
</Text>
</View>
<View className="absolute inset-x-0 bottom-0 p-4">
<Text className="text-base font-semibold text-white">Bottom sheet</Text>
<Text className="text-xs text-white/75">
Pro component · inertia dismissal, keyboard-aware
</Text>
</View>
</Press>
</Mount>
{/* Recent searches */}
<Mount delay={50}>
<View className="flex-row items-center justify-between px-6 pt-3">
<SectionLabel>Recent</SectionLabel>
{recents.length > 0 ? (
<Pressable
onPress={() => {
haptics.light();
clearRecents();
}}
accessibilityRole="button"
accessibilityLabel="Clear all recent searches"
hitSlop={8}
className="min-h-11 justify-center"
>
<Text variant="small" className="text-primary">
Clear all
</Text>
</Pressable>
) : null}
</View>
<View className="px-6 pt-1">
{recents.map((term) => (
<RecentRow
key={term}
term={term}
onPress={(t) => {
runSearch(t);
onRecentPress?.(t);
}}
onRemove={removeRecent}
/>
))}
{recents.length === 0 ? (
<Text variant="muted" className="py-2">
No recent searches.
</Text>
) : null}
</View>
</Mount>
{/* Trending */}
{trendingSearches.length > 0 ? (
<Mount delay={110}>
<View className="px-6 pt-6">
<SectionLabel>Trending</SectionLabel>
<View className="flex-row flex-wrap gap-2 pt-3">
{trendingSearches.map((term, i) => {
const tone = TRENDING_TONES[i % TRENDING_TONES.length] ?? TRENDING_TONES[0]!;
return (
<Chip
key={term}
variant="suggestion"
icon={<Icon name="flame" size="sm" color={tone.icon} />}
onPress={() => {
runSearch(term);
onTrendingPress?.(term);
}}
accessibilityLabel={`Search trending topic ${term}`}
className={cn("min-h-11 px-4", tone.bg)}
textClassName={tone.text}
>
{term}
</Chip>
);
})}
</View>
</View>
</Mount>
) : null}
{/* Suggested — result-style rows so the idle screen still ends
with content, the way App Store search does. */}
<Mount delay={170}>
<View className="px-6 pt-6">
<SectionLabel>Suggested</SectionLabel>
<View className="pt-2">
{DEFAULT_SUGGESTED.map((item, i) => (
<View key={item.id}>
<SearchResultRow item={item} onPress={onResultPress} />
{i < DEFAULT_SUGGESTED.length - 1 ? (
<View className="ml-[56px] h-px bg-border/60" />
) : null}
</View>
))}
</View>
</View>
</Mount>
</ScrollView>
) : (
<FlatList
data={activeResults}
keyExtractor={(item) => item.id}
keyboardShouldPersistTaps="handled"
contentContainerClassName="gap-1 px-6 pt-2"
contentContainerStyle={{ paddingBottom: insets.bottom + 32 }}
ListEmptyComponent={
<EmptyState
variant="search"
title="No results"
description={`Nothing matched “${trimmed}”. Try a different term.`}
action={
<Button size="sm" variant="secondary" onPress={() => updateQuery("")}>
Clear search
</Button>
}
className="py-20"
/>
}
renderItem={({ item, index }) => (
<Mount delay={Math.min(index * ROW_STAGGER, ROW_STAGGER_CAP)}>
<SearchResultRow item={item} onPress={onResultPress} />
</Mount>
)}
/>
)}
</SafeAreaView>
);
}
```
Screen: https://lumioui.dev/screens/core-search