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

Food Home Screen

Delivery home — address header with cart pill, search, cuisine chips, featured restaurant rail, and popular dishes with add buttons

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection · medium
$ npx lumioui add food-food-home
import { FoodHomeScreen } from "@/screens/food/food-home";
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
food/food-home.tsxsource · yours to edit
import * as React from "react";
import { useState } from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge, Card, Glass, Icon, Image, PromoBanner, SearchInput, Text } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { Mount, Press, SectionHead, World } from "../shared";

export interface FoodRestaurant {
  id: string;
  name: string;
  cuisine: string;
  /** Category key used by the filter chips. */
  category: string;
  rating: number;
  eta: string;
  /** "Free" renders as a success badge; anything else shows as a price. */
  deliveryFee: string;
  imageUrl?: string;
  badge?: string;
}

export interface FoodDish {
  id: string;
  name: string;
  restaurant: string;
  price: number;
  category: string;
  imageUrl?: string;
}

export interface FoodHomeScreenProps {
  /** Short delivery address shown in the header. */
  address?: string;
  categories?: string[];
  restaurants?: FoodRestaurant[];
  dishes?: FoodDish[];
  /** Item count on the cart affordance. Hidden when 0. */
  cartCount?: number;
  currency?: string;
  onSearch?: (query: string) => void;
  onCategoryPress?: (category: string) => void;
  onRestaurantPress?: (restaurant: FoodRestaurant) => void;
  onDishPress?: (dish: FoodDish) => void;
  onAddDish?: (dish: FoodDish) => void;
  onCartPress?: () => void;
  onAddressPress?: () => void;
  /** Promo banner CTA — "Order now". */
  onPromoPress?: () => void;
}

/** Pinned Unsplash food photography — each id visually verified to show
 *  the named dish, the same contract catalogImg keeps for apparel. */
const FOOD_PHOTOS = {
  burger: "photo-1568901346375-23c9450c58cd",
  pizza: "photo-1513104890138-7c749659a591",
  sushi: "photo-1579871494447-9811cf80d66c",
  tacos: "photo-1551504734-5ee1c4a1479b",
  ramen: "photo-1569718212165-3a8278d5f624",
  salad: "photo-1512621776951-a57141f2eefd",
  bowl: "photo-1546069901-ba9599a7e63c",
  coffee: "photo-1509042239860-f550ce710b93",
  dessert: "photo-1551024506-0bccd828d307",
} as const;

const img = (p: keyof typeof FOOD_PHOTOS, w = 640, h = 420) =>
  `https://images.unsplash.com/${FOOD_PHOTOS[p]}?w=${w}&h=${h}&fit=crop&q=70&auto=format`;

const fmt = (n: number, currency = "$") =>
  `${currency}${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;

const DEFAULT_CATEGORIES = ["All", "Pizza", "Sushi", "Burgers", "Healthy", "Mexican", "Coffee"];

/** Category → circle photo; "All" renders as an accent disc. */
const CATEGORY_PHOTOS: Record<string, string> = {
  Pizza: img("pizza", 160, 160),
  Sushi: img("sushi", 160, 160),
  Burgers: img("burger", 160, 160),
  Healthy: img("bowl", 160, 160),
  Mexican: img("tacos", 160, 160),
  Coffee: img("coffee", 160, 160),
};

const DEFAULT_RESTAURANTS: FoodRestaurant[] = [
  { id: "r1", name: "Nonna Rosa", cuisine: "Italian · $$", category: "Pizza", rating: 4.8, eta: "25–35 min", deliveryFee: "Free", imageUrl: img("pizza"), badge: "Popular" },
  { id: "r2", name: "Sakura Handroll Bar", cuisine: "Japanese · $$$", category: "Sushi", rating: 4.9, eta: "30–40 min", deliveryFee: "$1.99", imageUrl: img("sushi") },
  { id: "r3", name: "Smoky Stack", cuisine: "American · $", category: "Burgers", rating: 4.6, eta: "15–25 min", deliveryFee: "Free", imageUrl: img("burger"), badge: "New" },
  { id: "r4", name: "Green Ritual", cuisine: "Salads & bowls · $$", category: "Healthy", rating: 4.7, eta: "20–30 min", deliveryFee: "$0.99", imageUrl: img("bowl") },
  { id: "r5", name: "El Camino", cuisine: "Mexican · $", category: "Mexican", rating: 4.7, eta: "25–35 min", deliveryFee: "$1.49", imageUrl: img("tacos") },
  { id: "r6", name: "Kaffa Roasters", cuisine: "Coffee & brunch · $", category: "Coffee", rating: 4.9, eta: "10–20 min", deliveryFee: "Free", imageUrl: img("coffee") },
];

const DEFAULT_DISHES: FoodDish[] = [
  { id: "d1", name: "Margherita Verace", restaurant: "Nonna Rosa", price: 14.5, category: "Pizza", imageUrl: img("pizza", 240, 240) },
  { id: "d2", name: "Spicy Salmon Roll (8 pc)", restaurant: "Sakura Handroll Bar", price: 16.0, category: "Sushi", imageUrl: img("sushi", 240, 240) },
  { id: "d3", name: "Double Smash & Fries", restaurant: "Smoky Stack", price: 13.9, category: "Burgers", imageUrl: img("burger", 240, 240) },
  { id: "d4", name: "Harissa Chicken Bowl", restaurant: "Green Ritual", price: 12.75, category: "Healthy", imageUrl: img("bowl", 240, 240) },
  { id: "d5", name: "Tonkotsu Ramen", restaurant: "Menya Noodle", price: 15.4, category: "Sushi", imageUrl: img("ramen", 240, 240) },
  { id: "d6", name: "Al Pastor Trio", restaurant: "El Camino", price: 11.9, category: "Mexican", imageUrl: img("tacos", 240, 240) },
];

/** Cart affordance — secondary disc, count pill anchored to the corner. */
function CartButton({ count, onPress }: { count: number; onPress?: () => void }) {
  const haptics = useHaptic();
  return (
    <Press
      onPress={() => {
        haptics.light();
        onPress?.();
      }}
      accessibilityRole="button"
      accessibilityLabel={count > 0 ? `Cart, ${count} items` : "Cart"}
      hitSlop={8}
      className="h-11 w-11 items-center justify-center overflow-hidden rounded-full"
    >
      <Glass interactive effect="clear" sheen={0.45} className="absolute inset-0 rounded-full" pointerEvents="none" />
      <Icon name="shopping-cart" size="md" color="foreground" />
      {count > 0 ? (
        <View className="absolute -right-1 -top-1 min-w-5 items-center rounded-full bg-[#BEF04C] px-1.5 py-0.5">
          <Text className="text-[10px] font-semibold text-[#1B1B26] tabular-nums">
            {count}
          </Text>
        </View>
      ) : null}
    </Press>
  );
}
CartButton.displayName = "CartButton";

/** FoodHome — delivery home: address header with cart, search, category
 *  chips, a featured-restaurant rail, and a popular-dishes list. */
export function FoodHomeScreen({
  address = "128 Linden St, Apt 4B",
  categories = DEFAULT_CATEGORIES,
  restaurants = DEFAULT_RESTAURANTS,
  dishes = DEFAULT_DISHES,
  cartCount = 2,
  currency = "$",
  onSearch,
  onCategoryPress,
  onRestaurantPress,
  onDishPress,
  onAddDish,
  onCartPress,
  onAddressPress,
  onPromoPress,
}: FoodHomeScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [query, setQuery] = useState("");
  const [activeCategory, setActiveCategory] = useState(categories[0] ?? "All");

  const pickCategory = (category: string) => {
    setActiveCategory(category);
    // Chip fires haptics.selection() internally — don't double it.
    onCategoryPress?.(category);
  };

  const all = activeCategory === categories[0];
  const visibleRestaurants = all
    ? restaurants
    : restaurants.filter((r) => r.category === activeCategory);
  const visibleDishes = all ? dishes : dishes.filter((d) => d.category === activeCategory);

  return (
    <World primary="#65A30D">
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      {/* Address + cart header — the two things a delivery app always shows. */}
      <Mount>
        <View className="flex-row items-center gap-3 px-6 pb-2 pt-4">
          <Press
            onPress={() => {
              haptics.light();
              onAddressPress?.();
            }}
            accessibilityRole="button"
            accessibilityLabel={`Deliver to ${address}, change address`}
            hitSlop={8}
            className="min-h-11 flex-1 flex-row items-center gap-2"
          >
            <Icon name="map-pin" size="md" color="#65A30D" />
            <View className="flex-1">
              <Text variant="muted" className="text-xs">
                Deliver to
              </Text>
              <View className="flex-row items-center gap-1">
                <Text variant="small" className="font-semibold" numberOfLines={1}>
                  {address}
                </Text>
                <Icon name="chevron-down" size="sm" color="mutedForeground" />
              </View>
            </View>
          </Press>
          <CartButton count={cartCount} onPress={onCartPress} />
        </View>
      </Mount>

      {/* Search */}
      <Mount delay={40}>
        <View className="px-6 py-2">
          <SearchInput
            placeholder="Search dishes, restaurants…"
            value={query}
            debounceMs={300}
            onChangeText={(text) => {
              setQuery(text);
              onSearch?.(text);
            }}
            returnKeyType="search"
            accessibilityLabel="Search food"
          />
        </View>
      </Mount>

      {/* Cuisine rail — circular food photos with a label, the DoorDash
          idiom. Selected gets the accent ring instead of a filled chip. */}
      <Mount delay={80}>
        <ScrollView
          horizontal
          className="grow-0"
          showsHorizontalScrollIndicator={false}
          contentContainerClassName="gap-4 px-6 py-3"
          keyboardShouldPersistTaps="handled"
        >
          {categories.map((category) => {
            const selected = category === activeCategory;
            const photo = CATEGORY_PHOTOS[category];
            return (
              <Press
                key={category}
                onPress={() => pickCategory(category)}
                accessibilityRole="button"
                accessibilityState={{ selected }}
                accessibilityLabel={`Filter by ${category}`}
                className="w-14 items-center gap-1.5"
              >
                <View
                  className={cn(
                    "h-14 w-14 overflow-hidden rounded-full",
                    selected ? "border-2 border-[#65A30D]" : "border border-border"
                  )}
                  style={selected ? { padding: 0 } : undefined}
                >
                  {photo ? (
                    <Image
                      source={{ uri: photo }}
                      rounded="none"
                      className="h-full w-full"
                      accessibilityElementsHidden
                    />
                  ) : (
                    <View className="h-full w-full items-center justify-center bg-[#1B1B26]">
                      <Icon name="grid" size="sm" color="#BEF04C" />
                    </View>
                  )}
                </View>
                <Text
                  className={cn(
                    "text-xs",
                    selected ? "font-semibold text-[#65A30D]" : "text-muted-foreground"
                  )}
                  numberOfLines={1}
                >
                  {category}
                </Text>
              </Press>
            );
          })}
        </ScrollView>
      </Mount>

      <ScrollView
        className="flex-1"
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingBottom: insets.bottom + 88 }}
        keyboardShouldPersistTaps="handled"
      >
        {/* Promo banner — warm amber gradient, the food-app signature.
            Raw hex is art direction (like StoryChrome). */}
        <Mount delay={100}>
          <PromoBanner
            className="mx-6 mb-2 mt-1"
            eyebrow="Weekend deal"
            title="Free delivery over $20"
            subtitle="with code SUNDAY"
            ctaLabel="Order now"
            onCta={onPromoPress}
            icon="utensils"
            from="#F59E0B"
            to="#DC2626"
          />
        </Mount>

        {/* Featured restaurants — horizontal rail of photo-led cards. */}
        {/* Live-now line — food apps sell urgency and availability. */}
        <Mount delay={90}>
          <Text variant="muted" className="px-6 pt-1 text-xs">
            {restaurants.length} places delivering to you now · avg 25–35 min
          </Text>
        </Mount>

        {visibleRestaurants.length > 0 ? (
          <Mount delay={120}>
            <SectionHead title="Featured" action="See all" className="px-6 pb-3 pt-2" />
            <ScrollView
              horizontal
        className="grow-0"
              showsHorizontalScrollIndicator={false}
              contentContainerClassName="gap-4 px-6"
            >
              {visibleRestaurants.map((restaurant) => (
                <Card
                  variant="elevated"
                  key={restaurant.id}
                  onPress={() => onRestaurantPress?.(restaurant)}
                  accessibilityLabel={`${restaurant.name}, ${restaurant.cuisine}, rated ${restaurant.rating}`}
                  className="w-64 overflow-hidden rounded-3xl bg-white dark:bg-[#1A1A20]"
                >
                  <View>
                    <Image
                      source={restaurant.imageUrl ? { uri: restaurant.imageUrl } : undefined}
                      rounded="none"
                      aspectRatio={16 / 10}
                      accessibilityLabel={`Photo of food at ${restaurant.name}`}
                    />
                    {restaurant.badge ? (
                      <Badge variant="secondary" className="absolute left-3 top-3">
                        {restaurant.badge}
                      </Badge>
                    ) : null}
                  </View>
                  <Card.Content className="gap-1 p-3">
                    <Text variant="small" className="font-semibold" numberOfLines={1}>
                      {restaurant.name}
                    </Text>
                    <Text variant="muted" className="text-xs" numberOfLines={1}>
                      {restaurant.cuisine}
                    </Text>
                    <View className="mt-1 flex-row items-center gap-3">
                      <View
                        className="flex-row items-center gap-1"
                        accessibilityLabel={`Rated ${restaurant.rating} out of 5`}
                      >
                        <Icon name="star" size="sm" color="warning" filled />
                        <Text variant="small" className="tabular-nums">
                          {restaurant.rating.toFixed(1)}
                        </Text>
                      </View>
                      <View className="flex-row items-center gap-1">
                        <Icon name="clock" size="sm" color="mutedForeground" />
                        <Text variant="muted" className="text-xs">
                          {restaurant.eta}
                        </Text>
                      </View>
                      {restaurant.deliveryFee === "Free" ? (
                        <Badge variant="success" size="sm">
                          Free
                        </Badge>
                      ) : (
                        <Text variant="muted" className="text-xs tabular-nums">
                          {restaurant.deliveryFee}
                        </Text>
                      )}
                    </View>
                  </Card.Content>
                </Card>
              ))}
            </ScrollView>
          </Mount>
        ) : null}

        {/* Popular dishes — dense rows, thumbnail + price + add. */}
        {visibleDishes.length > 0 ? (
          <Mount delay={200}>
            <SectionHead title="Popular near you" action="See all" className="px-6 pb-1 pt-6" />
            <View className="px-6">
              {visibleDishes.map((dish) => (
                <Press
                  key={dish.id}
                  onPress={() => {
                    haptics.light();
                    onDishPress?.(dish);
                  }}
                  accessibilityRole="button"
                  accessibilityLabel={`${dish.name} from ${dish.restaurant}, ${fmt(dish.price, currency)}`}
                  className="min-h-11 flex-row items-center gap-3 py-3"
                >
                  <Image
                    source={dish.imageUrl ? { uri: dish.imageUrl } : undefined}
                    rounded="xl"
                    className="h-14 w-14"
                    accessibilityLabel={`Photo of ${dish.name}`}
                  />
                  <View className="flex-1">
                    <Text variant="small" numberOfLines={1}>
                      {dish.name}
                    </Text>
                    <Text variant="muted" className="text-xs" numberOfLines={1}>
                      {dish.restaurant}
                    </Text>
                  </View>
                  <Text variant="small" className="font-semibold tabular-nums">
                    {fmt(dish.price, currency)}
                  </Text>
                  <Press
                    onPress={() => {
                      haptics.medium();
                      onAddDish?.(dish);
                    }}
                    accessibilityRole="button"
                    accessibilityLabel={`Add ${dish.name} to cart`}
                    hitSlop={10}
                    className="h-11 w-11 items-center justify-center rounded-full bg-[#1B1B26]"
                  >
                    <Icon name="plus" size="md" color="#BEF04C" />
                  </Press>
                </Press>
              ))}
            </View>
          </Mount>
        ) : null}
      </ScrollView>
    </SafeAreaView>
    </World>
  );
}
FoodHomeScreen.displayName = "FoodHomeScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Food Home Screen" screen in React Native using LumioUI.

Goal: Delivery home — address header with cart pill, search, cuisine chips, featured restaurant rail, and popular dishes with add buttons 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 food-food-home
```

Or pull just the components: `npx lumioui add badge card chip icon image promo-banner 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

### `badge` — Compact status pill with variants, icons and pulse option.
- `<Badge>` ×2

### `card` — Compound surface — header, title, content and footer slots.
- `<Card>` ×1

### `chip` — Compact selectable pill with press scale and ink overlay.
- 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>` ×7

### `image` — Cached image with placeholder, fallback and aspect handling.
- `<Image>` ×3

### `promo-banner` — Gradient or photo promo card with eyebrow kicker, title, subtitle, and a glass CTA pill — the signature moment for home feeds.
- `<PromoBanner>` ×1

### `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>` ×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/food/food-home.tsx` exporting FoodHomeScreen. 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 { useState } from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge, Card, Glass, Icon, Image, PromoBanner, SearchInput, Text } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { Mount, Press, SectionHead, World } from "../shared";

export interface FoodRestaurant {
  id: string;
  name: string;
  cuisine: string;
  /** Category key used by the filter chips. */
  category: string;
  rating: number;
  eta: string;
  /** "Free" renders as a success badge; anything else shows as a price. */
  deliveryFee: string;
  imageUrl?: string;
  badge?: string;
}

export interface FoodDish {
  id: string;
  name: string;
  restaurant: string;
  price: number;
  category: string;
  imageUrl?: string;
}

export interface FoodHomeScreenProps {
  /** Short delivery address shown in the header. */
  address?: string;
  categories?: string[];
  restaurants?: FoodRestaurant[];
  dishes?: FoodDish[];
  /** Item count on the cart affordance. Hidden when 0. */
  cartCount?: number;
  currency?: string;
  onSearch?: (query: string) => void;
  onCategoryPress?: (category: string) => void;
  onRestaurantPress?: (restaurant: FoodRestaurant) => void;
  onDishPress?: (dish: FoodDish) => void;
  onAddDish?: (dish: FoodDish) => void;
  onCartPress?: () => void;
  onAddressPress?: () => void;
  /** Promo banner CTA — "Order now". */
  onPromoPress?: () => void;
}

/** Pinned Unsplash food photography — each id visually verified to show
 *  the named dish, the same contract catalogImg keeps for apparel. */
const FOOD_PHOTOS = {
  burger: "photo-1568901346375-23c9450c58cd",
  pizza: "photo-1513104890138-7c749659a591",
  sushi: "photo-1579871494447-9811cf80d66c",
  tacos: "photo-1551504734-5ee1c4a1479b",
  ramen: "photo-1569718212165-3a8278d5f624",
  salad: "photo-1512621776951-a57141f2eefd",
  bowl: "photo-1546069901-ba9599a7e63c",
  coffee: "photo-1509042239860-f550ce710b93",
  dessert: "photo-1551024506-0bccd828d307",
} as const;

const img = (p: keyof typeof FOOD_PHOTOS, w = 640, h = 420) =>
  `https://images.unsplash.com/${FOOD_PHOTOS[p]}?w=${w}&h=${h}&fit=crop&q=70&auto=format`;

const fmt = (n: number, currency = "$") =>
  `${currency}${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;

const DEFAULT_CATEGORIES = ["All", "Pizza", "Sushi", "Burgers", "Healthy", "Mexican", "Coffee"];

/** Category → circle photo; "All" renders as an accent disc. */
const CATEGORY_PHOTOS: Record<string, string> = {
  Pizza: img("pizza", 160, 160),
  Sushi: img("sushi", 160, 160),
  Burgers: img("burger", 160, 160),
  Healthy: img("bowl", 160, 160),
  Mexican: img("tacos", 160, 160),
  Coffee: img("coffee", 160, 160),
};

const DEFAULT_RESTAURANTS: FoodRestaurant[] = [
  { id: "r1", name: "Nonna Rosa", cuisine: "Italian · $$", category: "Pizza", rating: 4.8, eta: "25–35 min", deliveryFee: "Free", imageUrl: img("pizza"), badge: "Popular" },
  { id: "r2", name: "Sakura Handroll Bar", cuisine: "Japanese · $$$", category: "Sushi", rating: 4.9, eta: "30–40 min", deliveryFee: "$1.99", imageUrl: img("sushi") },
  { id: "r3", name: "Smoky Stack", cuisine: "American · $", category: "Burgers", rating: 4.6, eta: "15–25 min", deliveryFee: "Free", imageUrl: img("burger"), badge: "New" },
  { id: "r4", name: "Green Ritual", cuisine: "Salads & bowls · $$", category: "Healthy", rating: 4.7, eta: "20–30 min", deliveryFee: "$0.99", imageUrl: img("bowl") },
  { id: "r5", name: "El Camino", cuisine: "Mexican · $", category: "Mexican", rating: 4.7, eta: "25–35 min", deliveryFee: "$1.49", imageUrl: img("tacos") },
  { id: "r6", name: "Kaffa Roasters", cuisine: "Coffee & brunch · $", category: "Coffee", rating: 4.9, eta: "10–20 min", deliveryFee: "Free", imageUrl: img("coffee") },
];

const DEFAULT_DISHES: FoodDish[] = [
  { id: "d1", name: "Margherita Verace", restaurant: "Nonna Rosa", price: 14.5, category: "Pizza", imageUrl: img("pizza", 240, 240) },
  { id: "d2", name: "Spicy Salmon Roll (8 pc)", restaurant: "Sakura Handroll Bar", price: 16.0, category: "Sushi", imageUrl: img("sushi", 240, 240) },
  { id: "d3", name: "Double Smash & Fries", restaurant: "Smoky Stack", price: 13.9, category: "Burgers", imageUrl: img("burger", 240, 240) },
  { id: "d4", name: "Harissa Chicken Bowl", restaurant: "Green Ritual", price: 12.75, category: "Healthy", imageUrl: img("bowl", 240, 240) },
  { id: "d5", name: "Tonkotsu Ramen", restaurant: "Menya Noodle", price: 15.4, category: "Sushi", imageUrl: img("ramen", 240, 240) },
  { id: "d6", name: "Al Pastor Trio", restaurant: "El Camino", price: 11.9, category: "Mexican", imageUrl: img("tacos", 240, 240) },
];

/** Cart affordance — secondary disc, count pill anchored to the corner. */
function CartButton({ count, onPress }: { count: number; onPress?: () => void }) {
  const haptics = useHaptic();
  return (
    <Press
      onPress={() => {
        haptics.light();
        onPress?.();
      }}
      accessibilityRole="button"
      accessibilityLabel={count > 0 ? `Cart, ${count} items` : "Cart"}
      hitSlop={8}
      className="h-11 w-11 items-center justify-center overflow-hidden rounded-full"
    >
      <Glass interactive effect="clear" sheen={0.45} className="absolute inset-0 rounded-full" pointerEvents="none" />
      <Icon name="shopping-cart" size="md" color="foreground" />
      {count > 0 ? (
        <View className="absolute -right-1 -top-1 min-w-5 items-center rounded-full bg-[#BEF04C] px-1.5 py-0.5">
          <Text className="text-[10px] font-semibold text-[#1B1B26] tabular-nums">
            {count}
          </Text>
        </View>
      ) : null}
    </Press>
  );
}
CartButton.displayName = "CartButton";

/** FoodHome — delivery home: address header with cart, search, category
 *  chips, a featured-restaurant rail, and a popular-dishes list. */
export function FoodHomeScreen({
  address = "128 Linden St, Apt 4B",
  categories = DEFAULT_CATEGORIES,
  restaurants = DEFAULT_RESTAURANTS,
  dishes = DEFAULT_DISHES,
  cartCount = 2,
  currency = "$",
  onSearch,
  onCategoryPress,
  onRestaurantPress,
  onDishPress,
  onAddDish,
  onCartPress,
  onAddressPress,
  onPromoPress,
}: FoodHomeScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [query, setQuery] = useState("");
  const [activeCategory, setActiveCategory] = useState(categories[0] ?? "All");

  const pickCategory = (category: string) => {
    setActiveCategory(category);
    // Chip fires haptics.selection() internally — don't double it.
    onCategoryPress?.(category);
  };

  const all = activeCategory === categories[0];
  const visibleRestaurants = all
    ? restaurants
    : restaurants.filter((r) => r.category === activeCategory);
  const visibleDishes = all ? dishes : dishes.filter((d) => d.category === activeCategory);

  return (
    <World primary="#65A30D">
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      {/* Address + cart header — the two things a delivery app always shows. */}
      <Mount>
        <View className="flex-row items-center gap-3 px-6 pb-2 pt-4">
          <Press
            onPress={() => {
              haptics.light();
              onAddressPress?.();
            }}
            accessibilityRole="button"
            accessibilityLabel={`Deliver to ${address}, change address`}
            hitSlop={8}
            className="min-h-11 flex-1 flex-row items-center gap-2"
          >
            <Icon name="map-pin" size="md" color="#65A30D" />
            <View className="flex-1">
              <Text variant="muted" className="text-xs">
                Deliver to
              </Text>
              <View className="flex-row items-center gap-1">
                <Text variant="small" className="font-semibold" numberOfLines={1}>
                  {address}
                </Text>
                <Icon name="chevron-down" size="sm" color="mutedForeground" />
              </View>
            </View>
          </Press>
          <CartButton count={cartCount} onPress={onCartPress} />
        </View>
      </Mount>

      {/* Search */}
      <Mount delay={40}>
        <View className="px-6 py-2">
          <SearchInput
            placeholder="Search dishes, restaurants…"
            value={query}
            debounceMs={300}
            onChangeText={(text) => {
              setQuery(text);
              onSearch?.(text);
            }}
            returnKeyType="search"
            accessibilityLabel="Search food"
          />
        </View>
      </Mount>

      {/* Cuisine rail — circular food photos with a label, the DoorDash
          idiom. Selected gets the accent ring instead of a filled chip. */}
      <Mount delay={80}>
        <ScrollView
          horizontal
          className="grow-0"
          showsHorizontalScrollIndicator={false}
          contentContainerClassName="gap-4 px-6 py-3"
          keyboardShouldPersistTaps="handled"
        >
          {categories.map((category) => {
            const selected = category === activeCategory;
            const photo = CATEGORY_PHOTOS[category];
            return (
              <Press
                key={category}
                onPress={() => pickCategory(category)}
                accessibilityRole="button"
                accessibilityState={{ selected }}
                accessibilityLabel={`Filter by ${category}`}
                className="w-14 items-center gap-1.5"
              >
                <View
                  className={cn(
                    "h-14 w-14 overflow-hidden rounded-full",
                    selected ? "border-2 border-[#65A30D]" : "border border-border"
                  )}
                  style={selected ? { padding: 0 } : undefined}
                >
                  {photo ? (
                    <Image
                      source={{ uri: photo }}
                      rounded="none"
                      className="h-full w-full"
                      accessibilityElementsHidden
                    />
                  ) : (
                    <View className="h-full w-full items-center justify-center bg-[#1B1B26]">
                      <Icon name="grid" size="sm" color="#BEF04C" />
                    </View>
                  )}
                </View>
                <Text
                  className={cn(
                    "text-xs",
                    selected ? "font-semibold text-[#65A30D]" : "text-muted-foreground"
                  )}
                  numberOfLines={1}
                >
                  {category}
                </Text>
              </Press>
            );
          })}
        </ScrollView>
      </Mount>

      <ScrollView
        className="flex-1"
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingBottom: insets.bottom + 88 }}
        keyboardShouldPersistTaps="handled"
      >
        {/* Promo banner — warm amber gradient, the food-app signature.
            Raw hex is art direction (like StoryChrome). */}
        <Mount delay={100}>
          <PromoBanner
            className="mx-6 mb-2 mt-1"
            eyebrow="Weekend deal"
            title="Free delivery over $20"
            subtitle="with code SUNDAY"
            ctaLabel="Order now"
            onCta={onPromoPress}
            icon="utensils"
            from="#F59E0B"
            to="#DC2626"
          />
        </Mount>

        {/* Featured restaurants — horizontal rail of photo-led cards. */}
        {/* Live-now line — food apps sell urgency and availability. */}
        <Mount delay={90}>
          <Text variant="muted" className="px-6 pt-1 text-xs">
            {restaurants.length} places delivering to you now · avg 25–35 min
          </Text>
        </Mount>

        {visibleRestaurants.length > 0 ? (
          <Mount delay={120}>
            <SectionHead title="Featured" action="See all" className="px-6 pb-3 pt-2" />
            <ScrollView
              horizontal
        className="grow-0"
              showsHorizontalScrollIndicator={false}
              contentContainerClassName="gap-4 px-6"
            >
              {visibleRestaurants.map((restaurant) => (
                <Card
                  variant="elevated"
                  key={restaurant.id}
                  onPress={() => onRestaurantPress?.(restaurant)}
                  accessibilityLabel={`${restaurant.name}, ${restaurant.cuisine}, rated ${restaurant.rating}`}
                  className="w-64 overflow-hidden rounded-3xl bg-white dark:bg-[#1A1A20]"
                >
                  <View>
                    <Image
                      source={restaurant.imageUrl ? { uri: restaurant.imageUrl } : undefined}
                      rounded="none"
                      aspectRatio={16 / 10}
                      accessibilityLabel={`Photo of food at ${restaurant.name}`}
                    />
                    {restaurant.badge ? (
                      <Badge variant="secondary" className="absolute left-3 top-3">
                        {restaurant.badge}
                      </Badge>
                    ) : null}
                  </View>
                  <Card.Content className="gap-1 p-3">
                    <Text variant="small" className="font-semibold" numberOfLines={1}>
                      {restaurant.name}
                    </Text>
                    <Text variant="muted" className="text-xs" numberOfLines={1}>
                      {restaurant.cuisine}
                    </Text>
                    <View className="mt-1 flex-row items-center gap-3">
                      <View
                        className="flex-row items-center gap-1"
                        accessibilityLabel={`Rated ${restaurant.rating} out of 5`}
                      >
                        <Icon name="star" size="sm" color="warning" filled />
                        <Text variant="small" className="tabular-nums">
                          {restaurant.rating.toFixed(1)}
                        </Text>
                      </View>
                      <View className="flex-row items-center gap-1">
                        <Icon name="clock" size="sm" color="mutedForeground" />
                        <Text variant="muted" className="text-xs">
                          {restaurant.eta}
                        </Text>
                      </View>
                      {restaurant.deliveryFee === "Free" ? (
                        <Badge variant="success" size="sm">
                          Free
                        </Badge>
                      ) : (
                        <Text variant="muted" className="text-xs tabular-nums">
                          {restaurant.deliveryFee}
                        </Text>
                      )}
                    </View>
                  </Card.Content>
                </Card>
              ))}
            </ScrollView>
          </Mount>
        ) : null}

        {/* Popular dishes — dense rows, thumbnail + price + add. */}
        {visibleDishes.length > 0 ? (
          <Mount delay={200}>
            <SectionHead title="Popular near you" action="See all" className="px-6 pb-1 pt-6" />
            <View className="px-6">
              {visibleDishes.map((dish) => (
                <Press
                  key={dish.id}
                  onPress={() => {
                    haptics.light();
                    onDishPress?.(dish);
                  }}
                  accessibilityRole="button"
                  accessibilityLabel={`${dish.name} from ${dish.restaurant}, ${fmt(dish.price, currency)}`}
                  className="min-h-11 flex-row items-center gap-3 py-3"
                >
                  <Image
                    source={dish.imageUrl ? { uri: dish.imageUrl } : undefined}
                    rounded="xl"
                    className="h-14 w-14"
                    accessibilityLabel={`Photo of ${dish.name}`}
                  />
                  <View className="flex-1">
                    <Text variant="small" numberOfLines={1}>
                      {dish.name}
                    </Text>
                    <Text variant="muted" className="text-xs" numberOfLines={1}>
                      {dish.restaurant}
                    </Text>
                  </View>
                  <Text variant="small" className="font-semibold tabular-nums">
                    {fmt(dish.price, currency)}
                  </Text>
                  <Press
                    onPress={() => {
                      haptics.medium();
                      onAddDish?.(dish);
                    }}
                    accessibilityRole="button"
                    accessibilityLabel={`Add ${dish.name} to cart`}
                    hitSlop={10}
                    className="h-11 w-11 items-center justify-center rounded-full bg-[#1B1B26]"
                  >
                    <Icon name="plus" size="md" color="#BEF04C" />
                  </Press>
                </Press>
              ))}
            </View>
          </Mount>
        ) : null}
      </ScrollView>
    </SafeAreaView>
    </World>
  );
}
FoodHomeScreen.displayName = "FoodHomeScreen";
```

Screen: https://lumioui.dev/screens/food-food-home
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗