# Scanner

The scanner is the part of a Wrapped people come back to. Some scenes of the video start out hidden somewhere in the year: achievements that were earned, an easter egg or two. A beam sweeps the calendar, locks onto a day, a card flies out and turns over, and that scene joins the video. Use it when part of the story should be found, not just watched.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/scanner/
>
> Press the button. Click a card to turn it, click again to add it.

## Installation

```bash
pnpm add @react-wrapped/scan @react-wrapped/core gsap
```

```tsx
import "@react-wrapped/core/styles.css";
import "@react-wrapped/scan/styles.css";
```

## What is in it

- [YearScan](https://bettertyped.github.io/react-wrapped/docs/scanner/year-scan/): The panel: the year, the beam, the cards and the list of finds.
- [Rarity and drops](https://bettertyped.github.io/react-wrapped/docs/scanner/drops/): What a hidden scene is, the four rarities, and which drops a year gets.
- [useOpenedDrops](https://bettertyped.github.io/react-wrapped/docs/scanner/use-opened-drops/): Remembers what was opened, on this device or for this visit.

## How a find becomes a scene

1. You decide which scenes are hidden and describe each as a `HiddenDrop`: a name, a rarity, a line and pixel art.
2. The scan gets all of them in `drops` and the ids already opened in `found`.
3. When a card is turned over the scan calls `onOpen(id)`.
4. You add the id to `found` and build your scenes again, this time with that drop's scene in them. Give that scene the key `drop-` followed by the drop's id.
5. The player rebuilds only what changed, and the card flies to the timeline segment with that key.

So a drop is never a special kind of scene. It is an ordinary scene, usually a [fact scene](https://bettertyped.github.io/react-wrapped/docs/scenes/fact/), that your code leaves out until its id is in `found`.

## Scan first

For someone's own year the scan can come before the video. With `opening.gate` on `WrappedView`, a creator who has opened nothing gets the scan as the whole page. It says "Your Wrapped is almost ready." and offers one button. When the scan has run and found something, the video takes its place and the scan moves under it. Anyone who has scanned before, and anyone only watching a shared link, gets the video straight away.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/scanner/
>
> The whole page, starting from the scan.

`scan-first.tsx`

```tsx
import { useMemo, useState } from "react";

import {
  type CalendarLevels,
  DEFAULT_WRAPPED_CONFIG,
  type HiddenDrop,
  type SceneInstance,
  useOpenedDrops,
  type WrappedConfig,
  type WrappedSubject,
  WrappedView,
} from "@react-wrapped/kit";

/** What your director returns for one year, given the drops opened so far. */
interface Cut {
  subject: WrappedSubject;
  seed: string;
  /** The story, with a scene for every drop that has been opened. */
  scenes: readonly SceneInstance[];
  calendar: CalendarLevels;
  /** Every hidden scene, found or not. */
  drops: readonly HiddenDrop[];
}

export function ScanFirst({
  direct,
  describeDay,
  storageKey,
}: {
  direct: (opened: readonly string[]) => Cut;
  describeDay: (day: number) => string;
  /** Where the opened drops are remembered on this device. Null keeps them for this visit only. */
  storageKey: string | null;
}) {
  const [config, setConfig] = useState<WrappedConfig>(DEFAULT_WRAPPED_CONFIG);
  const { opened, open, ready } = useOpenedDrops(storageKey);
  // Directed again whenever a drop is opened: that is how a found scene gets into the video.
  const cut = useMemo(() => direct(opened), [direct, opened]);
  // Until storage has been read, nobody knows whether this creator has scanned before.
  if (!ready) return null;
  return (
    <WrappedView
      subject={cut.subject}
      seed={cut.seed}
      scenes={cut.scenes}
      config={config}
      onConfigChange={setConfig}
      opening={{
        calendar: cut.calendar,
        drops: cut.drops,
        found: opened,
        onOpen: open,
        describeDay,
        // The scan is the page until it has run once. Without this the video plays and the scan sits under it.
        gate: true,
      }}
    />
  );
}
```

`opening` is everything `WrappedView` needs to run the scan for you:

| Key | Type | What it does |
| --- | --- | --- |
| `calendar` | `CalendarLevels` | Handed on to the scan. |
| `drops` | `readonly HiddenDrop[]` | Handed on to the scan. |
| `found` | `readonly string[]` | Handed on to the scan. |
| `onOpen` | `(id: string) => void` | Handed on to the scan. |
| `describeDay` | `(day: number) => string` | Handed on to the scan. |
| `actions?` | `ReactNode` | Handed on to the scan. |
| `sessionKey?` | `string \| null` | Handed on to the scan. |
| `gate?` | `boolean` | Puts the scan first. Until the creator has run it once, the scan is the page and the video is not shown. |

The [full example](https://bettertyped.github.io/react-wrapped/examples/full-wrapped/) has a switch for both starts. To run the same flow without `WrappedView`, see [build the page from parts](https://bettertyped.github.io/react-wrapped/docs/guides/from-parts/).

## Related

- [YearScan](https://bettertyped.github.io/react-wrapped/docs/scanner/year-scan/)
- [Rarity and drops](https://bettertyped.github.io/react-wrapped/docs/scanner/drops/)
- [Fact scene](https://bettertyped.github.io/react-wrapped/docs/scenes/fact/)
- [Build the page from parts](https://bettertyped.github.io/react-wrapped/docs/guides/from-parts/)
- [Full Wrapped example](https://bettertyped.github.io/react-wrapped/examples/full-wrapped/)

Source: https://bettertyped.github.io/react-wrapped/docs/scanner/
