v0.1 public beta early access on GitHub
Home / Screens / media-serene-home
Screen template

Serene Home

Serene Home screen.

pro Expo Go ✓ iosandroid gestures: tap haptics: light · selection
$ npx lumioui add media-serene-home
import { SereneHomeScreen } from "@/screens/media/serene-home";
Registry dependencies4
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
media/serene-home.tsxsource · yours to edit
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>
  );
}
agent prompt — paste into Claude Code, Cursor, Devin…
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
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗