# Example: Range selection

The two selection slices side by side: cell ranges controlled through PretableSelectionFor<typeof columns> and onSelectionChange, and the rowSelectionColumn checkbox set reported by onRowSelectionChange — shift-click extends a range, Cmd/Ctrl-click adds a discontiguous one, dragging marquees a rectangle, and a caption under each shows which callback just fired.

Source: https://pretable.ai/examples/range-selection.md

```tsx RangeSelectionGrid.tsx
"use client";

import { useState } from "react";

import { PretableSurface } from "@pretable/react";
import type {
  PretableCellRangeFor,
  PretableSelectionFor,
} from "@pretable/react";

import { columns } from "./columns";
import { rows } from "./data";

const VIEWPORT_HEIGHT = 300;

// Echoes `PretableCellRangeFor`'s own fields — the shape the page's
// "Selection model" section describes below — rather than a re-derived summary,
// so the caption stays an honest window onto the controlled state above it.
// Narrowed to `typeof columns`, so a typo'd column id here is a compile error
// rather than a silently-dead comparison.
function describeRange(range: PretableCellRangeFor<typeof columns>): string {
  return range.startRowId === range.endRowId &&
    range.startColumnId === range.endColumnId
    ? `${range.startColumnId}@${range.startRowId}`
    : `${range.startColumnId}@${range.startRowId} → ${range.endColumnId}@${range.endRowId}`;
}

export function RangeSelectionGrid() {
  // The CELL-RANGE slice. Controlled, and narrowed to the column tuple.
  const [selection, setSelection] = useState<
    PretableSelectionFor<typeof columns>
  >({
    ranges: [],
    anchor: null,
  });
  // The CHECKBOX slice, which is a different thing entirely: it lives in the
  // engine, is reported by its own callback, and never appears in `selection`.
  // Wiring only `onSelectionChange` — the mistake this example exists to make
  // hard — leaves the checkboxes ticking with nothing downstream ever hearing.
  const [checkedRowIds, setCheckedRowIds] = useState<readonly string[]>([]);

  return (
    <div>
      <p style={{ margin: "0 0 8px", fontSize: 13 }}>
        Click a cell, then <kbd>Shift</kbd>+click another to extend the range.{" "}
        <kbd>Cmd</kbd>/<kbd>Ctrl</kbd>+click adds a discontiguous range. Drag
        from one cell to another for a marquee selection. Then tick a checkbox,
        and watch the second caption move instead of the first.
      </p>
      <PretableSurface
        ariaLabel="Selection demo"
        columns={columns}
        getRowId={(row) => row.id}
        rows={rows}
        rowSelectionColumn={{ enabled: true }}
        state={{ selection }}
        onSelectionChange={setSelection}
        onRowSelectionChange={setCheckedRowIds}
        viewportHeight={VIEWPORT_HEIGHT}
      />
      <p style={{ margin: "8px 0 0", fontSize: 13 }}>
        Cell ranges (<code>onSelectionChange</code>):{" "}
        <code>
          {selection.ranges.length > 0
            ? selection.ranges.map(describeRange).join(" · ")
            : "(none)"}
        </code>
      </p>
      <p style={{ margin: "4px 0 0", fontSize: 13 }}>
        Ticked rows (<code>onRowSelectionChange</code>):{" "}
        <code>
          {checkedRowIds.length > 0 ? checkedRowIds.join(", ") : "(none)"}
        </code>
      </p>
    </div>
  );
}
```

```ts columns.ts
import { createColumnHelper } from "@pretable/core";

import type { Row } from "./data";

// `createColumnHelper` + `as const` — the idiom the "Selection model" section
// on this page teaches — produces a `readonly` literal-id column tuple. That
// tuple is what narrows the controlled `state.selection` prop's `columnId` to
// a checked union via `PretableSelectionFor<typeof columns>`, rather than the
// broad `startColumnId: string` on `@pretable/core`'s `PretableCellRange`.
const column = createColumnHelper<Row>();

export const columns = [
  column.accessor("name", { type: "text", header: "Name" }),
  column.accessor("city", { type: "text", header: "City" }),
  column.accessor("region", { type: "text", header: "Region" }),
  column.accessor("status", { type: "enum", header: "Status" }),
] as const;
```

```ts data.ts
export interface Row {
  id: string;
  name: string;
  city: string;
  region: string;
  status: "ok" | "warn" | "error";
}

/**
 * A dozen rows across four columns — enough that a marquee drag spans a
 * genuine rectangle of cells and a shift-click range covers more than one
 * row, while still fitting the demo's fixed viewport without scrolling.
 */
export const rows: Row[] = [
  {
    id: "r1",
    name: "Ada Lovelace",
    city: "London",
    region: "EMEA",
    status: "ok",
  },
  {
    id: "r2",
    name: "Grace Hopper",
    city: "New York",
    region: "AMER",
    status: "ok",
  },
  {
    id: "r3",
    name: "Linus Torvalds",
    city: "Helsinki",
    region: "EMEA",
    status: "warn",
  },
  {
    id: "r4",
    name: "Margaret Hamilton",
    city: "Indianapolis",
    region: "AMER",
    status: "ok",
  },
  {
    id: "r5",
    name: "Alan Turing",
    city: "London",
    region: "EMEA",
    status: "error",
  },
  {
    id: "r6",
    name: "Katherine Johnson",
    city: "Hampton",
    region: "AMER",
    status: "ok",
  },
  {
    id: "r7",
    name: "Tim Berners-Lee",
    city: "London",
    region: "EMEA",
    status: "warn",
  },
  {
    id: "r8",
    name: "Radia Perlman",
    city: "Boston",
    region: "AMER",
    status: "ok",
  },
  {
    id: "r9",
    name: "Yukihiro Matsumoto",
    city: "Osaka",
    region: "APAC",
    status: "ok",
  },
  {
    id: "r10",
    name: "Hedy Lamarr",
    city: "Vienna",
    region: "EMEA",
    status: "ok",
  },
  {
    id: "r11",
    name: "Shigeru Miyamoto",
    city: "Kyoto",
    region: "APAC",
    status: "warn",
  },
  {
    id: "r12",
    name: "Barbara Liskov",
    city: "Boston",
    region: "AMER",
    status: "ok",
  },
];
```
