# Catalogs and shops

> Show one item at a time and swap it smoothly. A stage scene whose Model src changes with no blank frame, a camera that flies to frame each item, small scenes as tiles, waiting for a swap to be on screen, keeping items ready in hidden groups, and shadows that fit any item.

A catalog shows one big item with smaller ones to pick from: sneakers, furniture, a car configurator, a shop's next piece. Apricot handles the hard parts:

- **No blank frames.** When `<Model src>` changes, the item shown stays while the next one loads and its shaders compile, then the two swap in one frame.
- **The camera follows.** Children of the `<Model>` move onto the new item, so a `<CameraRig>` inside it flies to frame it over its `transition`.
- **Scenes share their work.** Every scene on screen uses one renderer, so an item a tile has already drawn shows on the stage with little or no compiling.

## A catalog

One stage scene, and a small scene per tile.

```tsx title="Catalog.tsx"
import { CameraRig, Scene } from "@apricot/react";
import { Model } from "@apricot/react/model";
import { useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";

const ITEMS = [
  { id: "chair", name: "Chair", src: require("./assets/models/chair.glb") },
  { id: "lamp", name: "Lamp", src: require("./assets/models/lamp.glb") },
  { id: "sofa", name: "Sofa", src: require("./assets/models/sofa.glb") },
];

export function Catalog() {
  const [item, setItem] = useState(ITEMS[0]);
  const [shown, setShown] = useState<number | string | null>(null);
  return (
    <View style={{ flex: 1 }}>
      <Scene style={{ flex: 1 }} look="studio">
        <Model src={item.src} ground onReady={(_, src) => setShown(src)}>
          <CameraRig view="three-quarter" orbit transition={0.6} />
        </Model>
      </Scene>
      <Text style={{ textAlign: "center", opacity: shown === item.src ? 1 : 0.4 }}>{item.name}</Text>
      <ScrollView horizontal contentContainerStyle={{ gap: 8, padding: 12 }}>
        {ITEMS.map((i) => (
          <Pressable key={i.id} onPress={() => setItem(i)} style={{ width: 96, height: 96 }}>
            {/* The tile's scene takes no touches: the Pressable does. */}
            <View pointerEvents="none" style={StyleSheet.absoluteFill}>
              <Scene style={{ flex: 1 }} look="studio">
                <Model src={i.src} ground>
                  <CameraRig view="three-quarter" />
                </Model>
              </Scene>
            </View>
          </Pressable>
        ))}
      </ScrollView>
    </View>
  );
}
```

- **`onReady(object, src)`** fires when an item is on screen: loaded, compiled and drawn. It is called again for each new `src`, with the `src` it is of (quick taps can skip some). Use it to fade in a caption, stop a spinner or take a picture.
- **`keepPrevious={false}`** empties the stage at once instead, and shows the next item when it is ready.
- **`transition`** on the `<CameraRig>` is how long the camera takes to frame the new item; `0` cuts straight to it. A new item is always framed afresh, wherever a finger left the camera.
- Scenes share their renderer when their lighting and shadow settings match (`look`, `lighting`, `lamps`, `shadows`). Keep them alike for scenes that show the same items.

## Waiting for a swap: `scene.drawn()`

After changing what a scene shows, `scene.drawn()` resolves once a frame shows everything just added. New things sit out a few frames while their shaders compile, with the last frame staying on screen, so wait for it before fading the view back in or taking a picture. It waits at most `timeout` ms (default 3000).

```tsx title="FadeOnSwap.tsx"
import { type ApricotScene, Box, Scene } from "@apricot/react";
import { useState } from "react";
import { Button } from "react-native";
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";

const COLORS = ["#ee9c9b", "#97ba80", "#a2bbcc"];

export function FadeOnSwap() {
  const [scene, setScene] = useState<ApricotScene | null>(null);
  const [index, setIndex] = useState(0);
  const fade = useSharedValue(1);
  const style = useAnimatedStyle(() => ({ opacity: fade.value }));
  const next = async () => {
    fade.value = withTiming(0.02, { duration: 150 });
    setIndex((i) => (i + 1) % COLORS.length);
    await scene?.drawn();
    fade.value = withTiming(1, { duration: 250 });
  };
  return (
    <>
      <Animated.View style={[{ flex: 1 }, style]}>
        <Scene style={{ flex: 1 }} onCreated={setScene}>
          <Box radius={0.1} color={COLORS[index]} />
        </Scene>
      </Animated.View>
      <Button title="Next" onPress={next} />
    </>
  );
}
```

> [!WARNING]
> Don't hide a scene by fading its view to opacity 0: it still draws, and on iOS it can hold up the app. Fade to a little above 0 (2%), or pause it with `<Scene paused>`.

`captureScene` waits for `drawn()` itself, so a picture taken right after a swap has the new item.

## Keeping items ready: hidden groups

For a small set of items, keep every one in the scene and show one at a time. A `<Group visible={false}>` is still warmed up with the scene, so showing it is instant, with no frame spent compiling.

```tsx title="Showcase.tsx"
import { Group, Scene } from "@apricot/react";
import { Model } from "@apricot/react/model";

const PIECES = [
  { id: "mug", src: require("./assets/models/mug.glb") },
  { id: "vase", src: require("./assets/models/vase.glb") },
];

export const Showcase = ({ shown }: { shown: string }) => (
  <Scene style={{ flex: 1 }} look="studio">
    {PIECES.map((p) => (
      <Group key={p.id} visible={p.id === shown}>
        <Model src={p.src} ground />
      </Group>
    ))}
  </Scene>
);
```

## A shadow that fits any item: `<ContactShadow under>`

`<ContactShadow under={object}>` (from `@apricot/assets`) sizes and lays itself under an object, again when it changes: one shadow fits a mug and a sofa alike. Pass the object itself, kept in state from a ref callback.

```tsx title="ShadowedItem.tsx"
import { ContactShadow } from "@apricot/assets";
import { Group, type SceneObject } from "@apricot/react";
import { Model } from "@apricot/react/model";
import { useState } from "react";

export function ShadowedItem({ src }: { src: number }) {
  const [item, setItem] = useState<SceneObject | null>(null);
  return (
    <Group ref={setItem}>
      <Model src={src} ground />
      <ContactShadow under={item} />
    </Group>
  );
}
```

`spread` (default 1.15) is how much wider than the footprint it reaches. For sizing anything else by an object (a label above it, a plinth under it), use [`useBounds`](https://matthiasbrandolin.dev/apricot/reference/hooks#usebounds).

## Framing items of different sizes

- `<CameraRig fit="footprint">` frames the floor plan only, so a tall item doesn't frame smaller than a flat one of the same size.
- `fit={{ footprint: floor }}` frames the floor plan of the object given (a plinth, a tile), ignoring what reaches past it.
- `fit={[1, 1.5, 1]}` frames a fixed box, so every item shows at one scale and sizes compare.

See [Cameras reference](https://matthiasbrandolin.dev/apricot/reference/cameras).

## Related

- [Loading models](https://matthiasbrandolin.dev/apricot/guides/models).
- [Pictures of the scene](https://matthiasbrandolin.dev/apricot/guides/capture): thumbnails instead of live tiles.
- [Scene reference](https://matthiasbrandolin.dev/apricot/reference/scene#several-scenes-and-scenes-off-screen): several scenes, `paused`.
