# View morph

> Moves the same shells from one layout to another. The shell moves, then both faces crossfade together.

- Name: `view-morph`
- Status: beta
- Categories: layout
- File: the install writes `view-morph.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/motion-timing`. The install adds them.
- Gallery: https://arbokui.com/components/view-morph
- Registry item: https://arbokui.com/r/view-morph.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/view-morph
   ```

## Use it for

- Two or more layouts of the same records, such as a board and a list, when each record must keep its element and its focus.
- Put one ViewMorphItem per record, with the same id in every layout, and swap the slot inside it.
- Put a ViewMorphGroup around each cluster. A header item uses layer="header". Its text is the same in every layout, and it does not crossfade.
- Mark the text that should fade with data-flip-slot. Leave the header's text unmarked.

## Avoid it for

- A one-time entrance. The first paint stores positions and does not move.
- Scaling a card to its next size. The shell animates its box, so a radius and a shadow are not stretched.
- Records that appear and disappear between layouts. A shell with no previous box does not fly in.

## Props

- `layout`: `string`. Default `"default"`. Which layout is showing. Changing it moves the shells.
- `contentLayouts`: `string[]`. Default `[]`. When both sides of a change are in this list, each slot stays fully opaque and its children crossfade. Every other change crossfades the slot.
- `className`: `string`. Classes on the root. Later classes win.
- `children`: `ReactNode`. Groups and shells.
- `Part. ViewMorphGroup`: `{ id: string; as?: "div" | "ul" | "tbody"; className?: string; children?: ReactNode }`. A group of shells. id is its stable key and is not copied onto the element's id. as picks "div", "ul", or "tbody".
- `Part. ViewMorphItem`: `{ id: string; layer?: "item" | "header"; as?: "div" | "li" | "tr"; className?: string; children?: ReactNode }`. One shell. id is its stable key and is not copied onto the element's id. The shell is not a tab stop. layer "header" stays above the others and is not crossfaded, so its text must be the same in every layout.

## Accessibility

- The root has no role of its own. The page that uses it gives it one, such as a tab panel.
- A shell is not in the tab order. Put the control that should take focus inside the shell.
- The shell moves with transform only, on --motion-duration-slow and --motion-ease-out, then rests. The ease never exceeds 1, so a frame-stepped strip shows no overshoot. Width and height do not animate.
- The fade follows --motion-duration-fast, capped at 120ms, and does not follow --motion-distance. A shell that moves waits until --motion-crossfade-delay. A shell that stays, and a reduced-motion switch, dissolve with no delay. That delay is the shared start. It is not a gap, and there is no row stagger.
- A header is not crossfaded. Its text has to be byte-identical across layouts.
- Motion off sets the durations to 0, and that drops the fade. Reduced motion sets --motion-distance to 0 and keeps the fade.

## Examples

### Two layouts of the same shells

```tsx
<ViewMorph layout={layout}>
  <ViewMorphGroup id="open">
    <ViewMorphItem id="open-label" layer="header">
      <div>Open</div>
    </ViewMorphItem>
    <ViewMorphItem id="ledger">
      <div data-flip-slot>{layout === "board" ? "Ledgerly payouts" : "Ledgerly payouts, list"}</div>
    </ViewMorphItem>
  </ViewMorphGroup>
</ViewMorph>
```
