{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pivot-table",
  "title": "Pivot Table",
  "description": "Dashboard-style analytics: pick which dimension becomes rows, which becomes columns, and how to aggregate (sum, average, count).",
  "dependencies": [
    "@tanstack/react-table"
  ],
  "registryDependencies": [
    "table",
    "select"
  ],
  "files": [
    {
      "path": "src/components/pivot/pivot.ts",
      "content": "export type SalesRecord = {\n  region: string\n  quarter: string\n  channel: string\n  amount: number\n}\n\nexport type AggregationType = 'sum' | 'avg' | 'count'\n\nexport function aggregate(\n  records: SalesRecord[],\n  aggFn: AggregationType,\n): number {\n  if (aggFn === 'count') return records.length\n  if (records.length === 0) return 0\n  const total = records.reduce((sum, r) => sum + r.amount, 0)\n  return aggFn === 'avg' ? total / records.length : total\n}\n\nexport interface PivotRow {\n  rowValue: string\n  cells: Record<string, number>\n  total: number\n}\n\nexport interface PivotResult {\n  colValues: string[]\n  rows: PivotRow[]\n  columnTotals: Record<string, number>\n  grandTotal: number\n}\n\nexport function pivotData(\n  data: SalesRecord[],\n  rowKey: keyof SalesRecord,\n  colKey: keyof SalesRecord,\n  aggFn: AggregationType,\n): PivotResult {\n  const rowValues = Array.from(new Set(data.map((d) => String(d[rowKey])))).sort()\n  const colValues = Array.from(new Set(data.map((d) => String(d[colKey])))).sort()\n\n  const rows: PivotRow[] = rowValues.map((rowValue) => {\n    const rowRecords = data.filter((d) => String(d[rowKey]) === rowValue)\n    const cells: Record<string, number> = {}\n    for (const colValue of colValues) {\n      cells[colValue] = aggregate(\n        rowRecords.filter((d) => String(d[colKey]) === colValue),\n        aggFn,\n      )\n    }\n    return { rowValue, cells, total: aggregate(rowRecords, aggFn) }\n  })\n\n  const columnTotals: Record<string, number> = {}\n  for (const colValue of colValues) {\n    columnTotals[colValue] = aggregate(\n      data.filter((d) => String(d[colKey]) === colValue),\n      aggFn,\n    )\n  }\n\n  return { colValues, rows, columnTotals, grandTotal: aggregate(data, aggFn) }\n}\n",
      "type": "registry:component",
      "target": "@components/tables/pivot-table/pivot.ts"
    },
    {
      "path": "src/components/pivot/data.ts",
      "content": "import type { SalesRecord } from './pivot'\n\nconst REGIONS = ['North', 'South', 'East', 'West']\nconst QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4']\nconst CHANNELS = ['Online', 'Retail']\n\nconst REGION_BASE: Record<string, number> = {\n  North: 42000,\n  South: 31000,\n  East: 55000,\n  West: 38000,\n}\n\nconst QUARTER_GROWTH: Record<string, number> = {\n  Q1: 1,\n  Q2: 1.08,\n  Q3: 0.95,\n  Q4: 1.22,\n}\n\nconst CHANNEL_SPLIT: Record<string, number> = {\n  Online: 0.6,\n  Retail: 0.4,\n}\n\nexport const salesData: SalesRecord[] = REGIONS.flatMap((region) =>\n  QUARTERS.flatMap((quarter) =>\n    CHANNELS.map((channel) => ({\n      region,\n      quarter,\n      channel,\n      amount: Math.round(\n        REGION_BASE[region] * QUARTER_GROWTH[quarter] * CHANNEL_SPLIT[channel],\n      ),\n    })),\n  ),\n)\n",
      "type": "registry:component",
      "target": "@components/tables/pivot-table/data.ts"
    },
    {
      "path": "src/components/pivot/data-table.tsx",
      "content": "'use client'\n\nimport { useMemo, useState } from 'react'\nimport {\n  flexRender,\n  getCoreRowModel,\n  useReactTable,\n} from '@tanstack/react-table'\nimport type { ColumnDef } from '@tanstack/react-table'\n\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableFooter,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '#/components/ui/table'\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '#/components/ui/select'\nimport type { AggregationType, PivotRow, SalesRecord } from './pivot'\nimport { pivotData } from './pivot'\n\nconst DIMENSIONS: { value: keyof SalesRecord; label: string }[] = [\n  { value: 'region', label: 'Region' },\n  { value: 'quarter', label: 'Quarter' },\n  { value: 'channel', label: 'Channel' },\n]\n\nconst AGGREGATIONS: { value: AggregationType; label: string }[] = [\n  { value: 'sum', label: 'Sum' },\n  { value: 'avg', label: 'Average' },\n  { value: 'count', label: 'Count' },\n]\n\nfunction formatValue(value: number, aggFn: AggregationType) {\n  if (aggFn === 'count') return value.toLocaleString('en-US')\n  return value.toLocaleString('en-US', {\n    style: 'currency',\n    currency: 'USD',\n    maximumFractionDigits: 0,\n  })\n}\n\ninterface PivotTableProps {\n  data: SalesRecord[]\n}\n\nexport function PivotTable({ data }: PivotTableProps) {\n  const [rowDim, setRowDim] = useState<keyof SalesRecord>('region')\n  const [colDim, setColDim] = useState<keyof SalesRecord>('quarter')\n  const [aggFn, setAggFn] = useState<AggregationType>('sum')\n\n  const { colValues, rows, columnTotals, grandTotal } = useMemo(\n    () => pivotData(data, rowDim, colDim, aggFn),\n    [data, rowDim, colDim, aggFn],\n  )\n\n  const rowLabel = DIMENSIONS.find((d) => d.value === rowDim)!.label\n\n  const columns = useMemo<ColumnDef<PivotRow>[]>(\n    () => [\n      {\n        id: 'rowValue',\n        header: rowLabel,\n        accessorKey: 'rowValue',\n        cell: ({ getValue }) => (\n          <span className=\"font-medium\">{getValue() as string}</span>\n        ),\n      },\n      ...colValues.map(\n        (colValue): ColumnDef<PivotRow> => ({\n          id: colValue,\n          header: colValue,\n          accessorFn: (row) => row.cells[colValue],\n          cell: ({ getValue }) => formatValue(getValue() as number, aggFn),\n        }),\n      ),\n      {\n        id: 'total',\n        header: 'Total',\n        accessorKey: 'total',\n        cell: ({ getValue }) => (\n          <span className=\"font-medium\">\n            {formatValue(getValue() as number, aggFn)}\n          </span>\n        ),\n      },\n    ],\n    [colValues, rowLabel, aggFn],\n  )\n\n  const table = useReactTable({\n    data: rows,\n    columns,\n    getCoreRowModel: getCoreRowModel(),\n  })\n\n  return (\n    <div>\n      <div className=\"mb-3 flex flex-wrap items-center gap-3\">\n        <div className=\"flex items-center gap-2\">\n          <span className=\"text-sm text-muted-foreground\">Rows</span>\n          <Select\n            value={rowDim}\n            onValueChange={(v) => setRowDim(v as keyof SalesRecord)}\n          >\n            <SelectTrigger className=\"w-32\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              {DIMENSIONS.filter((d) => d.value !== colDim).map((d) => (\n                <SelectItem key={d.value} value={d.value}>\n                  {d.label}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <span className=\"text-sm text-muted-foreground\">Columns</span>\n          <Select\n            value={colDim}\n            onValueChange={(v) => setColDim(v as keyof SalesRecord)}\n          >\n            <SelectTrigger className=\"w-32\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              {DIMENSIONS.filter((d) => d.value !== rowDim).map((d) => (\n                <SelectItem key={d.value} value={d.value}>\n                  {d.label}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <span className=\"text-sm text-muted-foreground\">Aggregate</span>\n          <Select\n            value={aggFn}\n            onValueChange={(v) => setAggFn(v as AggregationType)}\n          >\n            <SelectTrigger className=\"w-32\">\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              {AGGREGATIONS.map((a) => (\n                <SelectItem key={a.value} value={a.value}>\n                  {a.label}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </div>\n      </div>\n\n      <div className=\"overflow-hidden rounded-md border\">\n        <Table>\n          <TableHeader>\n            {table.getHeaderGroups().map((headerGroup) => (\n              <TableRow key={headerGroup.id}>\n                {headerGroup.headers.map((header) => (\n                  <TableHead key={header.id}>\n                    {header.isPlaceholder\n                      ? null\n                      : flexRender(\n                          header.column.columnDef.header,\n                          header.getContext(),\n                        )}\n                  </TableHead>\n                ))}\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {table.getRowModel().rows.map((row) => (\n              <TableRow key={row.id}>\n                {row.getVisibleCells().map((cell) => (\n                  <TableCell key={cell.id}>\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))}\n          </TableBody>\n          <TableFooter>\n            <TableRow>\n              <TableCell>Total</TableCell>\n              {colValues.map((colValue) => (\n                <TableCell key={colValue}>\n                  {formatValue(columnTotals[colValue], aggFn)}\n                </TableCell>\n              ))}\n              <TableCell>{formatValue(grandTotal, aggFn)}</TableCell>\n            </TableRow>\n          </TableFooter>\n        </Table>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/tables/pivot-table/data-table.tsx"
    },
    {
      "path": "src/components/pivot/index.tsx",
      "content": "import { salesData } from './data'\nimport { PivotTable } from './data-table'\n\nexport function PivotTableDemo() {\n  return <PivotTable data={salesData} />\n}\n",
      "type": "registry:component",
      "target": "@components/tables/pivot-table/index.tsx"
    }
  ],
  "type": "registry:block"
}