# YearScan

`YearScan` is the panel under the video with the year on it. Before the first scan the year is blank. Press the button and a beam colours it week by week, doubles back, and stops on a day. A card comes out of that cell, turns over, and lands in the video. Afterwards the panel lists the finds, and the scan can be played again.

> Live preview: try it at https://bettertyped.github.io/react-wrapped/docs/scanner/year-scan/
>
> The readout under the panel is the demo's own state.

## Installation

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

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

## Usage

The scan holds no list of finds. You give it `found` and it tells you through `onOpen` when there is one more. The component below is the preview above.

> One name is agreed between the scan and your scenes: the scene of a drop has the key `drop-` followed by the drop's id. A found card flies to the timeline segment with that key. With any other key the scene still joins the video, and the card fades out where it is.

`scan-the-year.tsx`

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

import { type CalendarLevels, resolvePalette, type WrappedConfig } from "@react-wrapped/core";
import { type HiddenDrop, useOpenedDrops, YearScan } from "@react-wrapped/scan";

export function ScanTheYear({
  calendar,
  drops,
  describeDay,
  config,
}: {
  /** The same calendar the video draws, so the scan hides its drops in the year the viewer just watched. */
  calendar: CalendarLevels;
  drops: readonly HiddenDrop[];
  /** How a day reads on a found card, by its index in the calendar: "Mar 14". */
  describeDay: (day: number) => string;
  config: WrappedConfig;
}) {
  // What has been opened so far. With `null` it is kept for this visit only. A key keeps it on this device.
  const { opened, open } = useOpenedDrops(null);
  const [running, setRunning] = useState(false);
  return (
    <div>
      <YearScan
        calendar={calendar}
        palette={resolvePalette(config)}
        drops={drops}
        found={opened}
        // Called when a card has been turned over and added. This is the moment its scene joins your video.
        onOpen={open}
        describeDay={describeDay}
        onRunChange={setRunning}
      />
      <output>
        {`${opened.length} of ${drops.length} found${running ? ", scanning" : ""}. opened: [${opened.join(", ")}]`}
      </output>
    </div>
  );
}
```

## Props

| Prop | Type | What it does |
| --- | --- | --- |
| `calendar` | `CalendarLevels` | The year the scan sweeps. Use the one the video draws, so both show the same year. |
| `palette` | `WrappedPalette` | The video's palette: `resolvePalette(config)`. The panel and the rarity colours follow it. |
| `drops` | `readonly HiddenDrop[]` | Every hidden scene, found or not, in the order the scan opens them. With none, nothing is drawn. |
| `found` | `readonly string[]` | The ids of the drops already opened. |
| `onOpen` | `(id: string) => void` | A card was turned over. Add the id to `found` and put that drop's scene in the video. |
| `describeDay` | `(day: number) => string` | How a day reads on a found card, by its index in `calendar.levels`. |
| `actions?` | `ReactNode` | Extra buttons for the panel's header, such as a link to every achievement. |
| `sessionKey?` | `string \| null` | A session storage key. With one, each find keeps its day for the visit, so a replay opens the same cells. |
| `gate?` | `boolean` | The scan is the page for now: the panel says the video is almost ready and offers nothing but the scan. |
| `onRunChange?` | `(running: boolean) => void` | A scan started, or came to rest: finished, stopped or skipped to its end. |
| `className?` | `string` | A class for the panel. |
| `classNames?` | `SlotClassNames<YearScanSlot>` | A class per part, by slot name: `"scan-start"`, `"scan-card"`. See [styling](https://bettertyped.github.io/react-wrapped/docs/guides/styling/). |

## What the viewer does

- **Scan my year** starts a run through every drop not found yet, one after another.
- **The card** comes out face down. A click turns it over, and a second click sends it into the video. Left alone it does both by itself, staying face up as long as its text takes to read.
- **Find all** skips the rest of a run and opens everything that is left.
- **A click outside the card** stops the run. A card already turned over still lands. One still face down goes back unopened.
- **Scan again** replays the finds once everything is found. Nothing new joins the video.
- **The badges** under the year list the finds. A click on one, or on its cell in the year, brings its card back up until Escape or another click.

## Where a drop hides

The scan picks the day, at random among the active days of the year, and no two drops share one. So a drop has no date of its own: `describeDay` only says how the day the scan picked reads on the card. With a `sessionKey` each find keeps its day for the rest of the visit, so replays open the same cells. Finds from an earlier visit get a day as soon as the panel mounts.

## gate and onRunChange

`gate` is for the [scan first](https://bettertyped.github.io/react-wrapped/docs/scanner/) flow. The panel fills the box the video is about to take, its headline says the Wrapped is almost ready, and a found card lands on its badge because there is no timeline yet. `onRunChange` tells you when a run starts and when it comes to rest. Keep `gate` on while a run you started from the gate is still going, or the first find swaps the page out from under the ones after it.

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

// Inside your page component, next to the list of found ids.
const [running, setRunning] = useState(false);
const [passed, setPassed] = useState(false);
// The scan is the page until one run has come to rest with something found.
const gated = !passed && (found.length === 0 || running);

const onRunChange = (now: boolean) => {
  setRunning(now);
  if (!now && found.length > 0) setPassed(true);
};
```

The [from parts example](https://bettertyped.github.io/react-wrapped/examples/from-parts/) has this wired into a whole page.

## Rarity on screen

Every find flares its cell in its rarity colour. The rarer the drop, the longer the card hums before it turns, and a legendary one gets the full show: a flash, a turning foil border and sparkles. The colours come from the palette, so they change with the theme. See [rarity and drops](https://bettertyped.github.io/react-wrapped/docs/scanner/drops/).

## Related

- [Scanner](https://bettertyped.github.io/react-wrapped/docs/scanner/)
- [Rarity and drops](https://bettertyped.github.io/react-wrapped/docs/scanner/drops/)
- [useOpenedDrops](https://bettertyped.github.io/react-wrapped/docs/scanner/use-opened-drops/)
- [Fact scene](https://bettertyped.github.io/react-wrapped/docs/scenes/fact/)
- [Timeline](https://bettertyped.github.io/react-wrapped/docs/player/timeline/)

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