# Example: Drag-to-group panel

The query is controlled here, so dragging a header onto the panel updates both the grid and the level list shown below it.

Source: https://pretable.ai/examples/grouping-panel.md

```tsx GroupingPanelGrid.tsx
"use client";

import { useState, type ComponentProps } from "react";

import { PretableSurface } from "@pretable/react";

import { columns } from "./columns";
import { type Position, positions } from "./data";

const VIEWPORT_HEIGHT = 340;

export function GroupingPanelGrid() {
  // The complete query is controlled so the current grouping levels can be
  // shown outside the grid. Omit both query props to let rows mode own it.
  const [query, setQuery] = useState<
    NonNullable<ComponentProps<typeof PretableSurface<Position>>["query"]>
  >({
    filters: [],
    sort: [],
    rowGroups: [{ columnId: "desk" }],
  });

  return (
    <div>
      <p style={{ margin: "0 0 8px", fontSize: 13 }}>
        Drag a header onto the strip to add a level · drag a chip to reorder · ✕
        or <kbd>Delete</kbd> removes one · click a ▾ to collapse a group
      </p>
      <PretableSurface<Position>
        ariaLabel="Positions grouped by desk"
        columns={columns}
        getRowId={(row) => row.id}
        groupPanel={{ enabled: true }}
        onQueryChange={setQuery}
        query={query}
        rows={positions}
        viewportHeight={VIEWPORT_HEIGHT}
      />
      <p style={{ margin: "8px 0 0", fontSize: 13 }}>
        Grouped by:{" "}
        <code>
          {query.rowGroups.length > 0
            ? query.rowGroups.map((entry) => entry.columnId).join(" → ")
            : "(nothing)"}
        </code>
      </p>
    </div>
  );
}
```

```ts columns.ts
import type { PretableColumn } from "@pretable/react";

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

const usd = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
});

const count = new Intl.NumberFormat("en-US");

export const columns: PretableColumn<Position>[] = [
  { id: "desk", header: "Desk" },
  { id: "sector", header: "Sector" },
  { id: "symbol", header: "Symbol" },
  {
    id: "shares",
    header: "Shares",
    type: "number",
    aggregate: "sum",
    format: ({ value }) => count.format(value as number),
    // NOT `format`. `PretableFormatInput.row` is non-optional and a group row
    // has no row behind it, so the cell formatter above is not a legal
    // aggregate formatter — this is the hook that is.
    formatAggregate: ({ value }) =>
      typeof value === "number" ? count.format(value) : "",
  },
  {
    id: "marketValue",
    header: "Market value",
    type: "number",
    aggregate: "sum",
    format: ({ value }) => usd.format(value as number),
    formatAggregate: ({ value }) =>
      typeof value === "number" ? usd.format(value) : "",
  },
];
```

```ts data.ts
export interface Position {
  id: string;
  desk: string;
  sector: string;
  symbol: string;
  shares: number;
  marketValue: number;
}

/**
 * Deliberately low cardinality on `desk` and `sector` — grouping is only
 * legible when a level has a handful of distinct keys, not hundreds.
 */
export const positions: Position[] = [
  {
    id: "p1",
    desk: "Equities",
    sector: "Technology",
    symbol: "NVDA",
    shares: 4200,
    marketValue: 512_400,
  },
  {
    id: "p2",
    desk: "Equities",
    sector: "Technology",
    symbol: "MSFT",
    shares: 1800,
    marketValue: 748_900,
  },
  {
    id: "p3",
    desk: "Equities",
    sector: "Healthcare",
    symbol: "LLY",
    shares: 620,
    marketValue: 486_100,
  },
  {
    id: "p4",
    desk: "Equities",
    sector: "Healthcare",
    symbol: "UNH",
    shares: 950,
    marketValue: 501_300,
  },
  {
    id: "p5",
    desk: "Equities",
    sector: "Energy",
    symbol: "XOM",
    shares: 3100,
    marketValue: 364_800,
  },
  {
    id: "p6",
    desk: "Credit",
    sector: "Financials",
    symbol: "JPM",
    shares: 2400,
    marketValue: 623_500,
  },
  {
    id: "p7",
    desk: "Credit",
    sector: "Financials",
    symbol: "GS",
    shares: 780,
    marketValue: 419_700,
  },
  {
    id: "p8",
    desk: "Credit",
    sector: "Energy",
    symbol: "CVX",
    shares: 1500,
    marketValue: 238_200,
  },
  {
    id: "p9",
    desk: "Macro",
    sector: "Financials",
    symbol: "TLT",
    shares: 5600,
    marketValue: 497_800,
  },
  {
    id: "p10",
    desk: "Macro",
    sector: "Energy",
    symbol: "USO",
    shares: 8800,
    marketValue: 611_600,
  },
  {
    id: "p11",
    desk: "Macro",
    sector: "Technology",
    symbol: "SMH",
    shares: 1250,
    marketValue: 329_400,
  },
  {
    id: "p12",
    desk: "Macro",
    sector: "Technology",
    symbol: "QQQ",
    shares: 900,
    marketValue: 452_700,
  },
];
```
