KQ-1724 · STACK/CARRY
Stack Carry
A kanban board where a selection moves as one stack. Dragging any selected card gathers the others under the pointer into a fan whose spread is fanAngle: the grabbed card follows 1:1 and each card behind it follows on its own spring, softer the deeper it sits, so a fast throw strings the deck out and a pause gathers it, while a count badge lands on springs.recoil. Dropping deals the cards into the column in their original board order on springs.glide, cascade() apart, as their dashed home slots fold shut and the cards around them FLIP to make room; refused cards fly home, shake once and say why. Shift-click selects a run, Cmd or Ctrl-click or the check tab toggles, X toggles the focused card, M opens a move menu that deals the same way, Escape clears, and a long press starts selecting on touch.
Coldbrook Support
Tuesday queue · 11 tickets
Arrows move between tickets, Shift with Up or Down extends the selection, X or Space selects, M moves the selection to a column, Escape clears it. Drag a selected ticket to carry them all.
2 selected · New
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/stack-carry
Props
- columns
- StackColumn[]
- default defaultStackColumns
- The columns, left to right: { id, title }.
- cards / defaultCards
- StackCard[]
- default — / defaultStackCards
- The cards, controlled or initial, in column order: { id, title, column, customer?, priority?, openedAt? }.
- onCardsChange
- (cards: StackCard[]) => void
- default —
- Fires with the whole board once a move has landed.
- selected / defaultSelected
- string[]
- default — / []
- The selected card ids, controlled or initial.
- onSelect
- (ids: string[]) => void
- default —
- Fires from the click, key or long press that changed the selection, with the ids in board order; also after a move, with the refused cards that stay selected.
- onMoveMany
- (ids: string[], toColumn: string, index: number) => StackMoveResult | void | Promise<StackMoveResult | void>
- default —
- Moves the cards. Answer now or later with { failed: [{ id, reason }] } for any that could not move; the rest land in board order at index among the column's other cards. While a promise is pending the stack waits and its badge spins.
- fanAngle
- number
- default 7
- How far the carried stack fans, in degrees per card; 0 is a neat pile.
- maxFan
- number
- default 5
- Cards drawn in the carried stack; the badge counts the rest.
- cardStyle
- "raised" | "flat" | "outline"
- default "raised"
- Card surface: raised with a shadow, a flat tint, or an outline on the board colour.
- accent
- string
- default "var(--accent-bright)"
- Selection border and check tab, the stack's badge, the drop wash and the Move button. Any CSS colour.
- longPressMs
- number
- default 450
- How long a finger holds a card before selecting starts, in ms.
- now
- number
- default Date.UTC(2026, 9, 13, 11)
- The moment ticket ages are read against, in ms. Never the wall clock.
- title
- string
- default "Coldbrook Support"
- The board's heading.
- subtitle
- string
- default "Tuesday queue"
- One line under the heading, followed by the ticket count.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the board has loaded: loading shows skeleton columns, error a message with Try again.
- onReload
- () => void
- default —
- Try again was pressed after the board failed to load.
- disabled
- boolean
- default false
- Read-only: cards can be selected but nothing moves.
- className
- string
- default —
- Classes for the outer container.
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | StackColumn[] | defaultStackColumns | The columns, left to right: { id, title }. |
| cards / defaultCards | StackCard[] | — / defaultStackCards | The cards, controlled or initial, in column order: { id, title, column, customer?, priority?, openedAt? }. |
| onCardsChange | (cards: StackCard[]) => void | — | Fires with the whole board once a move has landed. |
| selected / defaultSelected | string[] | — / [] | The selected card ids, controlled or initial. |
| onSelect | (ids: string[]) => void | — | Fires from the click, key or long press that changed the selection, with the ids in board order; also after a move, with the refused cards that stay selected. |
| onMoveMany | (ids: string[], toColumn: string, index: number) => StackMoveResult | void | Promise<StackMoveResult | void> | — | Moves the cards. Answer now or later with { failed: [{ id, reason }] } for any that could not move; the rest land in board order at index among the column's other cards. While a promise is pending the stack waits and its badge spins. |
| fanAngle | number | 7 | How far the carried stack fans, in degrees per card; 0 is a neat pile. |
| maxFan | number | 5 | Cards drawn in the carried stack; the badge counts the rest. |
| cardStyle | "raised" | "flat" | "outline" | "raised" | Card surface: raised with a shadow, a flat tint, or an outline on the board colour. |
| accent | string | "var(--accent-bright)" | Selection border and check tab, the stack's badge, the drop wash and the Move button. Any CSS colour. |
| longPressMs | number | 450 | How long a finger holds a card before selecting starts, in ms. |
| now | number | Date.UTC(2026, 9, 13, 11) | The moment ticket ages are read against, in ms. Never the wall clock. |
| title | string | "Coldbrook Support" | The board's heading. |
| subtitle | string | "Tuesday queue" | One line under the heading, followed by the ticket count. |
| status | "ready" | "loading" | "error" | "ready" | Whether the board has loaded: loading shows skeleton columns, error a message with Try again. |
| onReload | () => void | — | Try again was pressed after the board failed to load. |
| disabled | boolean | false | Read-only: cards can be selected but nothing moves. |
| className | string | — | Classes for the outer container. |