# Row Drag and Drop

Grid supports drag and drop for reordering rows and transferring rows between grids.

```tsx
import { createModel, insertElement } from "cx/data";
import { Controller } from "cx/ui";
import { DragHandle, Grid } from "cx/widgets";

interface Person {
  id: number;
  fullName: string;
  city: string;
}

interface PageModel {
  left: Person[];
  right: Person[];
  $record: Person;
}

const m = createModel<PageModel>();

class PageController extends Controller {
  onInit() {
    this.store.set(m.left, [
      { id: 1, fullName: "Alice Johnson", city: "New York" },
      { id: 2, fullName: "Bob Smith", city: "Los Angeles" },
      { id: 3, fullName: "Carol White", city: "Chicago" },
    ]);

    this.store.set(m.right, [
      { id: 4, fullName: "David Brown", city: "Houston" },
      { id: 5, fullName: "Eva Green", city: "Phoenix" },
    ]);
  }

  onDrop(e: any, targetPath: string) {
    const draggedRecords = e.source.records.map((r: any) => r.data);
    const sourcePath = e.source.data.source;
    let insertionIndex = e.target.insertionIndex;

    // Remove from source
    this.store.update(sourcePath, (records: Person[]) =>
      records.filter((r) => !draggedRecords.includes(r)),
    );

    // Adjust insertion index if moving within the same grid
    if (sourcePath === targetPath) {
      draggedRecords.forEach((record: any) => {
        const originalIndex = e.source.records.find(
          (r: any) => r.data === record,
        )?.index;
        if (originalIndex < insertionIndex) {
          insertionIndex--;
        }
      });
    }

    // Insert into target
    this.store.update(targetPath, (records: Person[]) => {
      let result = records;
      draggedRecords.forEach((record: any, i: number) => {
        result = insertElement(result, insertionIndex + i, record);
      });
      return result;
    });
  }
}

export default (
  <div controller={PageController} class="flex gap-4">
    <div class="flex-1">
      <h3 class="text-sm font-semibold mb-2">Team A</h3>
      <Grid
        records={m.left}
        style="width: 100%"
        border
        columns={[
          {
            header: "",
            align: "center",
            pad: false,
            items: (
              <DragHandle class="cursor-move text-gray-400 hover:text-gray-600 px-1">
                ⋮⋮
              </DragHandle>
            ),
          },
          { header: "Name", field: "fullName" },
          { header: "City", field: "city" },
        ]}
        dragSource={{ data: { type: "person", source: "left" } }}
        onDropTest={(e) => e.source?.data?.type === "person"}
        onDrop={(e, instance) =>
          instance.getControllerByType(PageController).onDrop(e, "left")
        }
      />
    </div>

    <div class="flex-1">
      <h3 class="text-sm font-semibold mb-2">Team B</h3>
      <Grid
        records={m.right}
        style="width: 100%"
        border
        columns={[
          { header: "Name", field: "fullName" },
          { header: "City", field: "city" },
        ]}
        row={{ style: { cursor: "move" } }}
        dragSource={{ data: { type: "person", source: "right" } }}
        dropZone={{ mode: "insertion" }}
        onDropTest={(e) => e.source?.data?.type === "person"}
        onDrop={(e, instance) =>
          instance.getControllerByType(PageController).onDrop(e, "right")
        }
      />
    </div>
  </div>
);

```

Drag rows to reorder within a grid or transfer between grids. Use the drag handle to initiate dragging.

## Drag Handle

Use `DragHandle` to create a specific grab area instead of making the entire row draggable:

```tsx
import { DragHandle } from "cx/widgets";

columns={[
  {
    header: "",
    items: (
      <DragHandle class="cursor-move text-gray-400 hover:text-gray-600 px-1">
        ⋮⋮
      </DragHandle>
    ),
  },
  // ... other columns
]}
```

This provides better control and prevents accidental drags when clicking on other parts of the row.

## Insertion Mode

Use `dropZone={{ mode: "insertion" }}` to show insertion indicators between rows:

```tsx
<Grid
  dragSource={{ data: { type: "record" } }}
  dropZone={{ mode: "insertion" }}
  onDropTest={(e) => /* validate drop */}
  onDrop={(e) => /* handle drop */}
/>
```

## Enabling Row Drag

Configure `dragSource` to make rows draggable:

```tsx
<Grid
  records={m.records}
  dragSource={{ data: { type: "record" } }}
  row={{ style: { cursor: "move" } }}
/>
```

## Handling Drops

Implement `onDropTest` to validate drops and `onDrop` to handle the reordering:

```tsx
<Grid
  onDropTest={(e) => e.source?.data?.type === "record"}
  onDrop={(e, { controller }) => controller.onDrop(e)}
/>
```

The `onDrop` handler receives information about the source records and target position:

```tsx
onDrop(e) {
  const draggedRecords = e.source.records.map((r) => r.data);
  const insertionIndex = e.target.insertionIndex;

  // Remove and reinsert records at new position
  this.store.update(m.records, (records) => {
    // ... reordering logic
  });
}
```

## Drag Source Configuration

| Property | Type | Description |
| -------- | ---- | ----------- |
| `data` | `object` | Custom data attached to the drag operation. |
| `mode` | `string` | Drag mode: `"move"` (default) or `"copy"`. |

## Drop Event Properties

| Property | Description |
| -------- | ----------- |
| `e.source.records` | Array of dragged record objects with `data` and `index`. |
| `e.source.data` | Custom data from `dragSource`. |
| `e.target.insertionIndex` | Index where items should be inserted. |

## Transferring Between Grids

Include a `source` identifier in the drag data to track which grid the row came from:

```tsx
dragSource={{ data: { type: "person", source: "left" } }}
```

In the drop handler, use the source to remove from the correct grid:

```tsx
onDrop(e, targetPath) {
  const sourcePath = e.source.data.source;

  // Remove from source grid
  this.store.update(sourcePath, (records) =>
    records.filter((r) => !draggedRecords.includes(r))
  );

  // Insert into target grid
  this.store.update(targetPath, (records) =>
    insertElement(records, insertionIndex, ...draggedRecords)
  );
}
```

## Configuration

| Property | Type | Description |
| -------- | ---- | ----------- |
| `dragSource` | `object` | Configuration for row dragging. |
| `dropZone` | `object` | Drop zone configuration. Use `mode: "insertion"` for visual indicators. |
| `onDropTest` | `function` | Test if a drop is valid. Return `true` to allow. |
| `onDrop` | `function` | Handle the drop operation. |

## Drop Zone Modes

| Mode | Description |
| ---- | ----------- |
| `preview` | Default mode. Shows a preview of the drop position. |
| `insertion` | Shows insertion lines between rows for reordering. |

See also: [Grid](/docs/tables/grid), [Tree Drag and Drop](/docs/tables/tree-drag-and-drop), [DragHandle](/docs/concepts/drag-handle)