Serene Home screen.
import { useState } from "react";
import { Image, ScrollView, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Avatar, Icon, Text, type IconName } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { catalogImg, HeroGradient, Mount, Press } from "../shared";
export interface SereneHomeScreenProps {
onNext?: () => void;
}
const POPULAR: { title: string; count: string; tag: string; by: string; img: string; locked?: boolean }[] = [
{ title: "STORIES THAT\nSPARK WONDER", count: "212.5K", tag: "Wonder", by: "Matthew McConaughey", img: catalogImg("game"), locked: true },
{ title: "EMDR-INSPIRED\nBILATERAL", count: "81.3K", tag: "EMDR Bilateral", by: "Curated by Lumio", img: catalogImg("game") },
];
const RECENT: { title: string; icon: IconName; tint: string; img?: string }[] = [
{ title: "Rain on leaves", icon: "droplet", tint: "#2E4B3E" },
{ title: "Night field", icon: "moon", tint: "#2E4B6E", img: catalogImg("game") },
{ title: "Golden hour", icon: "sun", tint: "#6E4B2E" },
];
function PopularCard({ item, index }: { item: (typeof POPULAR)[number]; index: number }) {
const haptics = useHaptic();
return (
<Mount delay={160 + index * 90} className="w-[52%] grow-0">
<Press onPress={() => haptics.light()} className="overflow-hidden rounded-[26px]">
<View className="h-64" style={{ backgroundColor: index === 0 ? "#3B4B7A" : "#7A2E1E" }}>
{index === 0 ? (
<Image source={{ uri: item.img }} className="absolute inset-0" resizeMode="cover" accessibilityIgnoresInvertColors />
) : (
<>
<View className="absolute -right-10 top-10 h-44 w-44 rounded-full bg-[#E2503B]/60" />
<View className="absolute -left-14 bottom-16 h-40 w-40 rounded-full bg-[#F0A92E]/40" />
</>
)}
<View className="absolute inset-x-3 top-3 flex-row items-center gap-2 rounded-full bg-white/[0.16] p-1.5 pr-3">
<Avatar size="sm" fallback={item.tag.slice(0,2)} source={{ uri: "https://i.pravatar.cc/80?img=32" }} />
<View className="flex-1">
<Text className="text-[11px] font-semibold text-white" numberOfLines={1}>{item.tag}</Text>
<Text className="text-[11px] text-white/70" numberOfLines={1}>{item.by}</Text>
</View>
{item.locked ? <Icon name="lock" size="sm" color="#FFFFFF" /> : null}
</View>
<View className="absolute inset-x-4 bottom-4">
<View className="mb-1.5 flex-row items-center gap-1">
<Icon name="user" size="sm" color="#FFFFFF" />
<Text className="text-[11px] font-medium text-white/85">{item.count}</Text>
</View>
<Text className="text-[14px] font-bold leading-5 text-white">{item.title}</Text>
</View>
</View>
</Press>
</Mount>
);
}
function NavItem({ icon, active }: { icon: IconName; active?: boolean }) {
const haptics = useHaptic();
return (
<Press onPress={() => haptics.light()} className="p-1.5">
<Icon name={icon} size="lg" color={active ? "#FFFFFF" : "#8A8A93"} />
</Press>
);
}
export function SereneHomeScreen({ onNext }: SereneHomeScreenProps) {
const haptics = useHaptic();
const [playing, setPlaying] = useState<number | null>(null);
return (
<View className="flex-1 bg-[#111114]">
<SafeAreaView className="flex-1" edges={["top"]}>
<Mount className="flex-row items-center justify-between px-6 pt-2" delay={40}>
<View className="flex-row items-center gap-2">
<Icon name="sparkles" size="md" color="#BEF04C" />
<Text className="text-[17px] font-bold text-white" style={{ fontStyle: "italic" }}>
Lumio
</Text>
</View>
<View className="flex-row items-center gap-3">
<Press onPress={() => haptics.light()} className="h-10 w-10 items-center justify-center rounded-full bg-white/[0.08]">
<Icon name="bell" size="md" color="#FFFFFF" />
</Press>
<Avatar size="md" fallback="Y" source={{ uri: "https://i.pravatar.cc/150?img=47" }} />
</View>
</Mount>
<Mount delay={90} className="mx-6 mt-4">
<Press
accessibilityRole="button" accessibilityLabel="Search"
onPress={() => { haptics.light(); onNext?.(); }}
className="h-12 flex-row items-center gap-3 rounded-full bg-white/[0.08] px-4"
>
<Icon name="search" size="md" color="#8A8A93" />
<Text className="text-[15px] text-[#8A8A93]">Search...</Text>
</Press>
</Mount>
<ScrollView showsVerticalScrollIndicator={false} contentContainerClassName="pb-40">
<Mount delay={130} className="mt-6 flex-row items-center justify-between px-6">
<Text className="text-[17px] font-semibold text-white">Popular on Lumio</Text>
<Text className="text-[12px] text-white/50">See All</Text>
</Mount>
<View className="mt-3 flex-row gap-3 px-6">
{POPULAR.map((p, i) => (
<PopularCard key={p.tag} item={p} index={i} />
))}
</View>
<Mount delay={260} className="mx-6 mt-5">
<Press onPress={() => haptics.light()} className="overflow-hidden rounded-[24px]">
<HeroGradient from="#FBC2EB" to="#FB923C" angle="horizontal" />
<View className="flex-row items-center justify-between px-5 py-4">
<View className="flex-1 pr-4">
<Text className="text-[17px] font-bold text-[#3B1B45]">Special Offer</Text>
<Text className="text-[12px] mt-1 leading-5 text-[#3B1B45]/80">
For a limited time, get 50% off over 10,000 sessions - our best deal ever.
</Text>
</View>
<View className="h-10 w-10 items-center justify-center rounded-full bg-white/50">
<Icon name="chevron-right" size="md" color="#3B1B45" />
</View>
</View>
</Press>
</Mount>
<Mount delay={330} className="mt-7 flex-row items-center justify-between px-6">
<Text className="text-[17px] font-semibold text-white">Recently Played</Text>
<Text className="text-[12px] text-white/50">See All</Text>
</Mount>
<View className="mt-3 flex-row gap-3 px-6">
{RECENT.map((r, i) => {
const live = playing === i;
return (
<Mount key={r.title} delay={370 + i * 70} className="flex-1">
<Press
accessibilityRole="button" accessibilityLabel={live ? `Pause ${r.title}` : `Play ${r.title}`}
onPress={() => {
haptics.light();
setPlaying(live ? null : i);
}}
className="h-36 overflow-hidden rounded-[20px]"
style={{ backgroundColor: r.tint }}
>
{r.img ? (
<Image source={{ uri: r.img }} className="absolute inset-0 opacity-85" resizeMode="cover" accessibilityIgnoresInvertColors />
) : (
<View className="absolute inset-0 items-center justify-center">
<Icon name={r.icon} size="xl" color="#FFFFFF" />
</View>
)}
{live ? <View className="absolute inset-0 border-[2.5px] border-white/80" /> : null}
<View className="absolute inset-x-3 bottom-3 flex-row items-center gap-1.5">
<Icon name={live ? "pause" : "play"} size="sm" color="#FFFFFF" />
<Text className="text-[11px] font-semibold text-white">{live ? "Playing" : "Play"}</Text>
</View>
</Press>
</Mount>
);
})}
</View>
</ScrollView>
<View className="absolute inset-x-14 bottom-9">
<View className="flex-row items-center justify-around rounded-full border border-white/[0.1] bg-[#17171B]/90 px-4 py-3.5">
<NavItem icon="home" active />
<NavItem icon="moon" />
<NavItem icon="play" />
<NavItem icon="user" />
</View>
</View>
</SafeAreaView>
</View>
);
}
Recreate the "Serene Home" screen in React Native using LumioUI.
Goal: Serene Home screen. 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 media-serene-home
```
Or pull just the components: `npx lumioui add avatar glass icon 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
### `avatar` — Circular user image with fallback initials and status dot.
- `<Avatar>` ×2
### `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>` ×9
### `text` — Themed type scale from caption to display.
- `<Text>` ×13
## 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/media/serene-home.tsx` exporting SereneHomeScreen. 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 { useState } from "react";
import { Image, ScrollView, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Avatar, Icon, Text, type IconName } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { catalogImg, HeroGradient, Mount, Press } from "../shared";
export interface SereneHomeScreenProps {
onNext?: () => void;
}
const POPULAR: { title: string; count: string; tag: string; by: string; img: string; locked?: boolean }[] = [
{ title: "STORIES THAT\nSPARK WONDER", count: "212.5K", tag: "Wonder", by: "Matthew McConaughey", img: catalogImg("game"), locked: true },
{ title: "EMDR-INSPIRED\nBILATERAL", count: "81.3K", tag: "EMDR Bilateral", by: "Curated by Lumio", img: catalogImg("game") },
];
const RECENT: { title: string; icon: IconName; tint: string; img?: string }[] = [
{ title: "Rain on leaves", icon: "droplet", tint: "#2E4B3E" },
{ title: "Night field", icon: "moon", tint: "#2E4B6E", img: catalogImg("game") },
{ title: "Golden hour", icon: "sun", tint: "#6E4B2E" },
];
function PopularCard({ item, index }: { item: (typeof POPULAR)[number]; index: number }) {
const haptics = useHaptic();
return (
<Mount delay={160 + index * 90} className="w-[52%] grow-0">
<Press onPress={() => haptics.light()} className="overflow-hidden rounded-[26px]">
<View className="h-64" style={{ backgroundColor: index === 0 ? "#3B4B7A" : "#7A2E1E" }}>
{index === 0 ? (
<Image source={{ uri: item.img }} className="absolute inset-0" resizeMode="cover" accessibilityIgnoresInvertColors />
) : (
<>
<View className="absolute -right-10 top-10 h-44 w-44 rounded-full bg-[#E2503B]/60" />
<View className="absolute -left-14 bottom-16 h-40 w-40 rounded-full bg-[#F0A92E]/40" />
</>
)}
<View className="absolute inset-x-3 top-3 flex-row items-center gap-2 rounded-full bg-white/[0.16] p-1.5 pr-3">
<Avatar size="sm" fallback={item.tag.slice(0,2)} source={{ uri: "https://i.pravatar.cc/80?img=32" }} />
<View className="flex-1">
<Text className="text-[11px] font-semibold text-white" numberOfLines={1}>{item.tag}</Text>
<Text className="text-[11px] text-white/70" numberOfLines={1}>{item.by}</Text>
</View>
{item.locked ? <Icon name="lock" size="sm" color="#FFFFFF" /> : null}
</View>
<View className="absolute inset-x-4 bottom-4">
<View className="mb-1.5 flex-row items-center gap-1">
<Icon name="user" size="sm" color="#FFFFFF" />
<Text className="text-[11px] font-medium text-white/85">{item.count}</Text>
</View>
<Text className="text-[14px] font-bold leading-5 text-white">{item.title}</Text>
</View>
</View>
</Press>
</Mount>
);
}
function NavItem({ icon, active }: { icon: IconName; active?: boolean }) {
const haptics = useHaptic();
return (
<Press onPress={() => haptics.light()} className="p-1.5">
<Icon name={icon} size="lg" color={active ? "#FFFFFF" : "#8A8A93"} />
</Press>
);
}
export function SereneHomeScreen({ onNext }: SereneHomeScreenProps) {
const haptics = useHaptic();
const [playing, setPlaying] = useState<number | null>(null);
return (
<View className="flex-1 bg-[#111114]">
<SafeAreaView className="flex-1" edges={["top"]}>
<Mount className="flex-row items-center justify-between px-6 pt-2" delay={40}>
<View className="flex-row items-center gap-2">
<Icon name="sparkles" size="md" color="#BEF04C" />
<Text className="text-[17px] font-bold text-white" style={{ fontStyle: "italic" }}>
Lumio
</Text>
</View>
<View className="flex-row items-center gap-3">
<Press onPress={() => haptics.light()} className="h-10 w-10 items-center justify-center rounded-full bg-white/[0.08]">
<Icon name="bell" size="md" color="#FFFFFF" />
</Press>
<Avatar size="md" fallback="Y" source={{ uri: "https://i.pravatar.cc/150?img=47" }} />
</View>
</Mount>
<Mount delay={90} className="mx-6 mt-4">
<Press
accessibilityRole="button" accessibilityLabel="Search"
onPress={() => { haptics.light(); onNext?.(); }}
className="h-12 flex-row items-center gap-3 rounded-full bg-white/[0.08] px-4"
>
<Icon name="search" size="md" color="#8A8A93" />
<Text className="text-[15px] text-[#8A8A93]">Search...</Text>
</Press>
</Mount>
<ScrollView showsVerticalScrollIndicator={false} contentContainerClassName="pb-40">
<Mount delay={130} className="mt-6 flex-row items-center justify-between px-6">
<Text className="text-[17px] font-semibold text-white">Popular on Lumio</Text>
<Text className="text-[12px] text-white/50">See All</Text>
</Mount>
<View className="mt-3 flex-row gap-3 px-6">
{POPULAR.map((p, i) => (
<PopularCard key={p.tag} item={p} index={i} />
))}
</View>
<Mount delay={260} className="mx-6 mt-5">
<Press onPress={() => haptics.light()} className="overflow-hidden rounded-[24px]">
<HeroGradient from="#FBC2EB" to="#FB923C" angle="horizontal" />
<View className="flex-row items-center justify-between px-5 py-4">
<View className="flex-1 pr-4">
<Text className="text-[17px] font-bold text-[#3B1B45]">Special Offer</Text>
<Text className="text-[12px] mt-1 leading-5 text-[#3B1B45]/80">
For a limited time, get 50% off over 10,000 sessions - our best deal ever.
</Text>
</View>
<View className="h-10 w-10 items-center justify-center rounded-full bg-white/50">
<Icon name="chevron-right" size="md" color="#3B1B45" />
</View>
</View>
</Press>
</Mount>
<Mount delay={330} className="mt-7 flex-row items-center justify-between px-6">
<Text className="text-[17px] font-semibold text-white">Recently Played</Text>
<Text className="text-[12px] text-white/50">See All</Text>
</Mount>
<View className="mt-3 flex-row gap-3 px-6">
{RECENT.map((r, i) => {
const live = playing === i;
return (
<Mount key={r.title} delay={370 + i * 70} className="flex-1">
<Press
accessibilityRole="button" accessibilityLabel={live ? `Pause ${r.title}` : `Play ${r.title}`}
onPress={() => {
haptics.light();
setPlaying(live ? null : i);
}}
className="h-36 overflow-hidden rounded-[20px]"
style={{ backgroundColor: r.tint }}
>
{r.img ? (
<Image source={{ uri: r.img }} className="absolute inset-0 opacity-85" resizeMode="cover" accessibilityIgnoresInvertColors />
) : (
<View className="absolute inset-0 items-center justify-center">
<Icon name={r.icon} size="xl" color="#FFFFFF" />
</View>
)}
{live ? <View className="absolute inset-0 border-[2.5px] border-white/80" /> : null}
<View className="absolute inset-x-3 bottom-3 flex-row items-center gap-1.5">
<Icon name={live ? "pause" : "play"} size="sm" color="#FFFFFF" />
<Text className="text-[11px] font-semibold text-white">{live ? "Playing" : "Play"}</Text>
</View>
</Press>
</Mount>
);
})}
</View>
</ScrollView>
<View className="absolute inset-x-14 bottom-9">
<View className="flex-row items-center justify-around rounded-full border border-white/[0.1] bg-[#17171B]/90 px-4 py-3.5">
<NavItem icon="home" active />
<NavItem icon="moon" />
<NavItem icon="play" />
<NavItem icon="user" />
</View>
</View>
</SafeAreaView>
</View>
);
}
```
Screen: https://lumioui.dev/screens/media-serene-home