# MovableCardGrid

> A card grid the reader can rearrange.

## Facts

- **Product**: Max
- **Family**: Pipeline
- **Global**: `window.DigitalCrew.MovableCardGrid`
- **Source**: `max-agent: src/features/prospects/ui/components/movable-card-grid.tsx`
- **Import in the app**: `import { MovableCardGrid } from "@/features/prospects/ui/components/movable-card-grid";`
- **Live preview**: /design/components/movable-card-grid/preview.html
- **Page**: /design/components/movable-card-grid

## Guidelines

In arrange mode each card gets a dashed scrim to drag and a handle with arrow buttons that move it one cell at a time.

**Use it for** the prospect detail page and the dashboard. Pair it with an "Arrange cards" / "Done" toggle.

**What you provide**:
- `cards: { key, title, className?, node, actions? }[]`. `className` sets column spans; `actions` adds controls to the handle after the arrows.
- `arranging: boolean`.
- `onMove(key, targetKey, edge: "before" | "after")`: the component doesn't reorder anything itself.
- `className`: the grid, for example `grid gap-4 sm:grid-cols-2 xl:grid-cols-3`.

**Behaviour**:
- It creates its own `DndContext` with a `PointerSensor` that starts after 6px of movement.
- Arrow moves use the rendered layout. Left and right stay within the row and do nothing when the card is alone in it. Up and down go to the nearest row. A card that spans a row jumps over it.
- There is no keyboard sensor: the arrow buttons are the accessible way to move cards.

**Anatomy**:
- In arrange mode cells are at least `min-h-28`.
- The scrim is `rounded-xl border-2 border-dashed border-primary/40 bg-background/60`.
- The handle is centred 8px from the top, `rounded-md border bg-background px-1.5 py-0.5 shadow-sm`, with a `GripVertical` icon, the title in `text-xs font-medium`, and 24px ghost icon buttons.
- A drop target gets `ring-2 ring-primary`.

**Don't** wrap it in a `DndContext`: it creates its own.

## API

```ts
export type MoveEdge = "before" | "after";
export type MoveDirection = "left" | "right" | "up" | "down";
export interface MovableGridCard {
    key: string;
    /** Shown on the arrange-mode handle so an empty card is still nameable. */
    title: string;
    /** Extra classes for the grid cell (column spans). */
    className?: string;
    node: ReactNode;
    /** Extra per-card controls on the arrange-mode handle, after the arrows. */
    actions?: ReactNode;
}
export declare function MovableCardGrid({ cards, arranging, onMove, className, }: {
    cards: MovableGridCard[];
    arranging: boolean;
    onMove: (key: string, targetKey: string, edge: MoveEdge) => void;
    className?: string;
}): JSX.Element;
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  function tile(title, Icon, lines) {
    return h(DC.Card, { className: "gap-2 py-4" },
      h(DC.CardHeader, { className: "px-4" },
        h(DC.CardTitle, { className: "flex items-center gap-2 text-sm" }, h(Icon, { className: "size-4 text-muted-foreground" }), title)),
      h(DC.CardContent, { className: "space-y-1 px-4 text-sm" },
        lines.map(function (l) {
          return h("div", { key: l[0], className: "flex justify-between gap-3" },
            h("span", { className: "text-muted-foreground" }, l[0]), h("span", { className: "truncate font-medium" }, l[1]));
        })));
  }
  var INITIAL = [
    { key: "contact", title: "Contact", node: tile("Contact", I.User, [["Name", "Claire Dubois"], ["Role", "CFO"], ["Email", "claire.dubois@northwind.com"]]) },
    { key: "company", title: "Company", node: tile("Company", I.Building2, [["Name", "Northwind Traders"], ["Size", "480 employees"], ["HQ", "Lyon, France"]]) },
    { key: "engagement", title: "Engagement", node: tile("Engagement", I.TrendingUp, [["Emails opened", "4 of 5"], ["Replies", "1"], ["LinkedIn", "Connected"]]) },
    { key: "next", title: "Next step", node: tile("Next step", I.Calendar, [["Meeting", "Thu 1 Oct, 10:00"], ["Owner", "Sarah Lindqvist"], ["Stage", "Interested"]]) }
  ];
  function move(cards, key, targetKey, edge) {
    var moving = cards.filter(function (c) { return c.key === key; })[0];
    var rest = cards.filter(function (c) { return c.key !== key; });
    var at = rest.findIndex(function (c) { return c.key === targetKey; });
    if (!moving || at === -1) return cards;
    rest.splice(edge === "after" ? at + 1 : at, 0, moving);
    return rest;
  }
  function App() {
    var s = React.useState(INITIAL), cards = s[0], setCards = s[1];
    var a = React.useState(true), arranging = a[0], setArranging = a[1];
    return h("div", { className: "space-y-4" },
      h("div", { className: "flex items-center justify-between" },
        h("p", { className: "text-sm font-semibold" }, "Claire Dubois"),
        h(DC.Button, { variant: arranging ? "default" : "outline", size: "sm", className: "gap-1.5", "aria-pressed": arranging,
          onClick: function () { setArranging(!arranging); } },
          h(I.LayoutDashboard, { className: "h-3.5 w-3.5" }), arranging ? "Done" : "Arrange cards")),
      h(DC.MovableCardGrid, {
        cards: cards, arranging: arranging, className: "grid gap-4 sm:grid-cols-2",
        onMove: function (key, targetKey, edge) { setCards(function (prev) { return move(prev, key, targetKey, edge); }); }
      }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in Pipeline

- [DealCard](/design/components/deal-card.md): One draggable deal on the deals board, plus `DealCardPreview`, the tilted copy shown under the cursor while dragging.
- [DealColumn](/design/components/deal-column.md): One column of the deals board: a droppable, infinitely scrolling list of `DealCard`s under a header with the stage dot, a count badge and money totals.
- [SalesWorkspaceMap](/design/components/sales-workspace-map.md): A read-only React Flow map of how a workspace's lists, signals, campaigns, rules and pipelines connect.
- [StageColors](/design/components/stage-colors.md): The 17-colour palette for pipeline and deal stage accents, with helpers that return static Tailwind classes and a hex value for each colour.
