# Position Slot

> One player in one position on a team sheet: their photo, their name, then "10 · Fly-half", with pick-then-place moves.

- Name: `position-slot`
- Status: beta
- Categories: sport, display
- File: the install writes `position-slot.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `lucide-react`. The install adds them.
- Gallery: https://arbokui.com/components/position-slot
- Registry item: https://arbokui.com/r/position-slot.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/position-slot
   ```

## Use it for

- Team sheets and depth charts: one slot per shirt number.
- Name positions with positionOf(format, number), for 15s, sevens, 10s, and U13. It says "Replacement" past the starters.
- Move players by pick, then place: a click on a player picks them up (held), then pass holding to every other slot. A click there swaps or places them.
- Mouse drags work too, through onPick and onDrop. Keep the moves in your own state either way.
- Set a player's status to injured or unavailable. It shows in place of the position name.
- Put the player's avatar in player.avatar.

## Avoid it for

- Drag as the only way to move someone. Always give onClick too.
- A plain list of people. Use a list or a table.
- Boxing each slot in a card. A slot has no box, so a sheet of them breathes.

## Props

- `number`: `number`. The shirt number, which says where they play.
- `position`: `string`. The position's name, like "Fly-half". positionOf("15s", 10) gives it.
- `player`: `{ person: string; avatar?: ReactNode; status?: "injured" | "unavailable" } | null`. Who plays here. null or left out when the position is empty.
- `holding`: `PositionSlotPlayer | null`. Someone picked up in another slot. This slot shows a dashed edge, and its hint says "Swap with …" or "Put … here".
- `held`: `boolean`. Default `false`. This slot's player is the one picked up.
- `onClick`: `() => void`. Picks up this player, or places the held one here. Without it the slot is not a button.
- `onPick`: `() => void`. A mouse drag started here.
- `onDrop`: `() => void`. Something was dropped here.
- `disabled`: `boolean`. Default `false`. Blocks clicks and drags.
- `className`: `string`. Extra classes. Later classes win.

## Accessibility

- With onClick it is a native button, so Enter and Space pick up and place, as a click does.
- A held slot has aria-pressed="true".
- The hint, like "Swap with Noah Kerr", stays in the button's name at all times, so screen readers hear what a press does.
- Injured and Not available are words, not only colors. They use the danger and warning soft text, which the theme holds to 4.5:1 on the page and cards.
- Photos are hidden from screen readers, since the name is right beside them.
- positionOf and positions come from a client file. Call them in client code, or pass position names from the server.

## Examples

### A read-only slot

```tsx
<PositionSlot number={10} position="Fly-half" player={{ person: "Noah Kerr" }} />
```

### Names from the format

```tsx
"use client";

import { PositionSlot, positionOf } from "@/components/ui/position-slot";

export function FrontRow() {
  return [1, 2, 3].map((number) => (
    <PositionSlot key={number} number={number} position={positionOf("15s", number)} />
  ));
}
```

### Pick, then place

```tsx
"use client";

import { useState } from "react";
import { PositionSlot, positionOf, type PositionSlotPlayer } from "@/components/ui/position-slot";

export function Backs() {
  const [sheet, setSheet] = useState<Record<number, PositionSlotPlayer | null>>({
    9: { person: "Sam Carter" },
    10: { person: "Noah Kerr" },
    12: null,
  });
  const [held, setHeld] = useState<number | null>(null);

  const press = (number: number) => {
    if (held === null) {
      if (sheet[number]) setHeld(number);
      return;
    }
    if (held !== number) setSheet({ ...sheet, [number]: sheet[held], [held]: sheet[number] });
    setHeld(null);
  };

  return Object.keys(sheet).map(Number).map((number) => (
    <PositionSlot
      key={number}
      number={number}
      position={positionOf("15s", number)}
      player={sheet[number]}
      held={held === number}
      holding={held !== null && held !== number ? sheet[held] : null}
      onClick={() => press(number)}
    />
  ));
}
```
