# Reorderable list

A list a person puts in order: by dragging an item's handle, from the keyboard (Space or Enter on the handle picks it up, the arrow keys move it, Space or Enter drops it, Escape puts it back), or from the menu beside the handle (Move up, Move down, Move to top, Move to bottom, Move to another group), for people who don't drag or use the arrow keys. With `groups`, items also move between groups: left and right from the keyboard, following the reading direction.

Every step is announced ("Billing, position 3 of 8 in Next", "Dropped", "Cancelled: Billing back at position 2"), and focus stays on the moved item's handle after a drop: the item is drawn in its new place, not drawn again. A move is only a preview until it's dropped, so your `onReorder` or `onMove` is called once, with where it ended. Items your app adds or removes are announced too, and when a removed item held focus, focus goes to the next item's handle.

- Registry name: `reorderable-list` (`@candor/reorderable-list`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/reorderable-list

## Install

```bash
npx candor-ui add reorderable-list
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/reorderable-list`.
It brings in: bidi, dropdown-menu, locale, locked, motion, tokens, use-announcer, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { ReorderableList, type ReorderableGroup } from "@/components/candor/reorderable-list"

type Check = { id: string; name: string; owner: string }

const checks: Check[] = [
  { id: "lint", name: "Lint", owner: "Build agent" },
  { id: "types", name: "Type check", owner: "Build agent" },
  { id: "unit", name: "Unit tests", owner: "Test agent" },
  { id: "e2e", name: "End-to-end tests", owner: "Test agent" },
]

/** One list: the order the checks run in. Drag a handle, or press Space on it and use the arrow keys, or use the menu. */
function Order() {
  const [items, setItems] = React.useState(checks)
  return (
    <ReorderableList
      label="Checks, in the order they run"
      items={items}
      keyOf={(c) => c.id}
      labelOf={(c) => c.name}
      onReorder={setItems}
      renderItem={(c, { index }) => (
        <div className="flex items-baseline gap-2 px-1 text-sm">
          <span className="w-4 text-xs tabular-nums text-muted-foreground">{index + 1}</span>
          <span className="font-medium">{c.name}</span>
          <span className="ms-auto text-xs text-muted-foreground">{c.owner}</span>
        </div>
      )}
      className="max-w-md"
    />
  )
}

/** Groups: checks move between Required and Optional, with the left and right arrow keys or the menu. */
function Groups() {
  const [groups, setGroups] = React.useState<ReorderableGroup<Check>[]>([
    { id: "required", label: "Required before merge", items: checks.slice(0, 3) },
    { id: "optional", label: "Optional", items: checks.slice(3) },
  ])
  const move = (key: string, to: string, index: number) =>
    setGroups((all) => {
      const item = all.flatMap((g) => g.items).find((c) => c.id === key)
      if (!item) return all
      return all.map((g) => {
        const rest = g.items.filter((c) => c.id !== key)
        return g.id === to ? { ...g, items: [...rest.slice(0, index), item, ...rest.slice(index)] } : { ...g, items: rest }
      })
    })
  return (
    <ReorderableList
      label="Merge checks"
      items={[]}
      groups={groups}
      keyOf={(c) => c.id}
      labelOf={(c) => c.name}
      onReorder={() => {}}
      onMove={move}
      renderItem={(c) => <span className="px-1 text-sm">{c.name}</span>}
      className="max-w-xl"
    />
  )
}

export default function Example() {
  return (
    <div className="flex flex-col gap-8">
      <Order />
      <Groups />
    </div>
  )
}
```

## <ReorderableList>

Props:

- `items: T[]`: The items, in order. With `groups`, the groups hold the items and this is not read: pass `[]`.
- `keyOf: (item: T) => string`: A stable key for an item, used to keep it in place while it moves.
- `labelOf: (item: T) => string`: What an item is called, for its handle ("Move Billing") and the announcements. Drawn and read as text.
- `renderItem: (item: T, state: { grabbed: boolean; index: number }) => React.ReactNode`: Draws an item's content beside its handle; `grabbed` is true while it is being moved, `index` is where it is.
- `onReorder: (items: T[]) => void`: Called with the items in their new order when a move is dropped (without `groups`).
- `label: string`: What the list is, for screen readers: "Steps", "Columns shown".
- `groups?: ReorderableGroup<T>[] | undefined`: Groups an item can move between, each with its own items. Moves within and between them go to `onMove`.
- `onMove?: ((key: string, toGroup: string, index: number) => void) | undefined`: With `groups`: called when a move is dropped, with the item's key, the group it went to and its index there.
- `locked?: string | undefined`: Why the list can't be reordered now. Every handle and menu stays reachable and says why.
- `className?: string | undefined`: Classes for the root, merged with Candor's.

## Strings (`reorderableList`)

The fixed words, English by default. Override any of them under `reorderableList` in `<LocaleProvider strings={{ reorderableList: { … } }}>`; the table is exported as `reorderableListStrings`.

- `handle`: Move {label}
- `menu`: Ways to move {label}
- `moveUp`: Move up
- `moveDown`: Move down
- `moveTop`: Move to top
- `moveBottom`: Move to bottom
- `moveTo`: Move to {group}
- `atTop`: It’s already first.
- `atBottom`: It’s already last.
- `grabbed`: {label} picked up, position {position} of {count}. Arrow keys move it, Space drops it, Escape puts it back.
- `grabbedIn`: {label} picked up, position {position} of {count} in {group}. Arrow keys move it, Space drops it, Escape puts it back.
- `at`: {label}, position {position} of {count}
- `atIn`: {label}, position {position} of {count} in {group}
- `dropped`: Dropped: {label}, position {position} of {count}
- `droppedIn`: Dropped: {label}, position {position} of {count} in {group}
- `cancelled`: Cancelled: {label} back at position {position}
- `cancelledIn`: Cancelled: {label} back at position {position} in {group}
- `added`: {label} added
- `removed`: {label} removed
- `addedMany`: { one: "{count} item added", other: "{count} items added" }
- `removedMany`: { one: "{count} item removed", other: "{count} items removed" }
- `emptyGroup`: Nothing here yet.

## Types

### ReorderableGroup

A group of a ReorderableList: its id, its name (drawn as text) and its items, in order.

```ts
{ id: string; label: string; items: T[] }
```

## Motion

- **grab** (pop, scale): An item picked up is framed, the frame popping in round it; a drawing (`swell`, `lift`, `tilt`, `ghost`) changes how.
- **item** (pop, scale): An item your app adds after the list first drew pops in, and one it removes pops out from its place.
- **settle** (move): After a move, the items that changed place slide from where they were. Under reduced motion they are simply in their new places.

Set with `--cd-motion-reorderable-list-<moment>-*` in your brand file; the families are in the design reference.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
