{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "description": "TanStack Table with manual sort, expansion and selection, responsive column priority and optional stacked rows. Desktop row activation uses one keyboard stop, with exact timestamps available on row focus or time hover.",
  "dependencies": [
    "@tanstack/react-table@8.21.3",
    "lucide-react@1.33.0"
  ],
  "registryDependencies": [
    "@waimakers/utils",
    "@waimakers/table",
    "@waimakers/button",
    "@waimakers/empty-state",
    "@waimakers/error-state",
    "@waimakers/refused-state",
    "@waimakers/skeleton-presets",
    "@waimakers/tooltip"
  ],
  "files": [
    {
      "path": "src/components/ui/data-table.tsx",
      "content": "import {\n  flexRender,\n  getCoreRowModel,\n  useReactTable,\n  type ColumnDef,\n  type ExpandedState,\n  type VisibilityState,\n} from \"@tanstack/react-table\";\nimport { ArrowUpDown, ChevronDown, ChevronRight, ChevronUp, Info } from \"lucide-react\";\nimport { Fragment, useEffect, useId, useMemo, useRef, useState, type ReactNode } from \"react\";\n\nimport { cn, focusRing } from \"../../lib/utils\";\nimport { Button } from \"./button\";\nimport { EmptyState } from \"./empty-state\";\nimport { ErrorState } from \"./error-state\";\nimport { RefusedState } from \"./refused-state\";\nimport { SkeletonTable } from \"./skeleton-presets\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"./tooltip\";\nimport { TimestampTooltipGroup, TimestampTooltipTarget } from \"./timestamp-tooltip-group\";\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from \"./table\";\n\nexport interface DataTableColumn<T> {\n  id: string;\n  header: string;\n  description?: string;\n  cell: (row: T) => ReactNode;\n  cellClassName?: string;\n  headerClassName?: string;\n  sortable?: boolean;\n  priority?: 1 | 2 | 3;\n}\n\nexport interface DataTableMobileLabels {\n  label: (columnId: string, content?: ReactNode) => ReactNode;\n}\n\nexport interface DataTableProps<T> {\n  columns: readonly DataTableColumn<T>[];\n  data: readonly T[];\n  getRowId: (row: T) => string;\n  getRowLabel?: (row: T) => string;\n  groupKey?: (row: T) => string;\n  groupLabel?: (key: string) => ReactNode;\n  sort?: { id: string; desc: boolean } | null;\n  onSort?: (columnId: string) => void;\n  renderExpanded?: (row: T) => ReactNode;\n  isRowSelected?: (row: T) => boolean;\n  onRowClick?: (row: T) => void;\n  rowActionPlacement?: \"first-cell\" | \"end\";\n  emptyState?: ReactNode;\n  filtered?: boolean;\n  onClearFilters?: () => void;\n  state?:\n    | { kind: \"loading\" }\n    | { kind: \"refused\"; grant: string; grantor: string; title?: string }\n    | { kind: \"error\"; title: string; description: string; reference: string; onRetry: () => void };\n  density?: \"comfortable\" | \"compact\";\n  scrollable?: boolean;\n  columnVisibility?: VisibilityState;\n  mobileRow?: (\n    row: T,\n    labels: DataTableMobileLabels,\n  ) => { primary: ReactNode; status?: ReactNode; secondary?: ReactNode };\n  className?: string;\n  label?: string;\n}\n\nexport function DataTable<T>({\n  columns,\n  data,\n  getRowId,\n  getRowLabel,\n  groupKey,\n  groupLabel,\n  sort,\n  onSort,\n  renderExpanded,\n  isRowSelected,\n  onRowClick,\n  rowActionPlacement = \"first-cell\",\n  emptyState,\n  filtered,\n  onClearFilters,\n  state,\n  density = \"compact\",\n  scrollable = false,\n  columnVisibility,\n  mobileRow,\n  className,\n  label,\n}: DataTableProps<T>) {\n  const root = useRef<HTMLDivElement>(null);\n  const instanceId = useId();\n  const [width, setWidth] = useState(1280);\n  const [expanded, setExpanded] = useState<ExpandedState>({});\n  useEffect(() => {\n    const element = root.current;\n    if (!element) {\n      return;\n    }\n    const observer = new ResizeObserver(([entry]) => {\n      if (entry) {\n        setWidth(entry.contentRect.width);\n      }\n    });\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, []);\n  const tableColumns = useMemo<ColumnDef<T>[]>(\n    () =>\n      columns.map((column) => ({\n        id: column.id,\n        header: column.header,\n        cell: ({ row }) => column.cell(row.original),\n        enableSorting: column.sortable ?? false,\n      })),\n    [columns],\n  );\n  const tableData = useMemo(() => [...data], [data]);\n  const visibility = Object.fromEntries(\n    columns.map((column) => [\n      column.id,\n      columnVisibility?.[column.id] !== false &&\n        (column.priority === undefined ||\n          column.priority === 1 ||\n          width >= (column.priority === 2 ? 960 : 1200)),\n    ]),\n  );\n  const selection = Object.fromEntries(\n    data.filter((row) => isRowSelected?.(row)).map((row) => [getRowId(row), true]),\n  );\n  const table = useReactTable({\n    data: tableData,\n    columns: tableColumns,\n    getRowId,\n    getCoreRowModel: getCoreRowModel(),\n    getRowCanExpand: () => Boolean(renderExpanded),\n    onExpandedChange: setExpanded,\n    manualSorting: true,\n    state: {\n      expanded,\n      columnVisibility: visibility,\n      rowSelection: selection,\n      sorting: sort ? [sort] : [],\n    },\n  });\n  const rows = table.getRowModel().rows;\n  const groupHeading = (index: number) => {\n    const row = rows[index];\n    if (!groupKey || !row) {\n      return undefined;\n    }\n    const key = groupKey(row.original);\n    const previous = rows[index - 1];\n    return previous && groupKey(previous.original) === key ? undefined : (groupLabel?.(key) ?? key);\n  };\n  const endAction = Boolean(onRowClick && rowActionPlacement === \"end\");\n  const span =\n    table.getVisibleLeafColumns().length + (renderExpanded ? 1 : 0) + (endAction ? 1 : 0);\n  const stacked = mobileRow !== undefined && width < 768;\n  const plainEmpty = !state && !filtered && rows.length === 0 && typeof emptyState === \"string\";\n  const stateContent =\n    state?.kind === \"loading\" ? (\n      <SkeletonTable cols={columns.length} />\n    ) : state?.kind === \"refused\" ? (\n      <RefusedState grant={state.grant} grantor={state.grantor} title={state.title} />\n    ) : state?.kind === \"error\" ? (\n      <ErrorState {...state} />\n    ) : rows.length === 0 && !plainEmpty ? (\n      filtered ? (\n        <EmptyState\n          title=\"No results for these filters\"\n          action={\n            onClearFilters ? (\n              <Button type=\"button\" variant=\"outline\" onClick={onClearFilters}>\n                Clear filters\n              </Button>\n            ) : undefined\n          }\n        />\n      ) : (\n        (emptyState ?? <EmptyState title=\"No items yet\" />)\n      )\n    ) : undefined;\n  return (\n    <div\n      ref={root}\n      data-slot=\"data-table\"\n      data-density={density}\n      data-layout={stacked ? \"stacked\" : \"table\"}\n      className=\"min-w-0 rounded-lg border border-border\"\n    >\n      {stateContent !== undefined ? (\n        stateContent\n      ) : stacked && !plainEmpty ? (\n        <ul aria-label={label} className=\"divide-y divide-border\">\n          {rows.map((row, index) => {\n            const content = mobileRow(row.original, mobileLabels(columns));\n            const open = row.getIsExpanded();\n            const previewId = `${instanceId}-preview-${row.id}`;\n            const rowLabel = getRowLabel?.(row.original) ?? row.id;\n            const heading = groupHeading(index);\n            return (\n              <Fragment key={row.id}>\n                {heading !== undefined && (\n                  <li role=\"presentation\" className=\"bg-muted/40 px-3 py-2\">\n                    <h3 className=\"text-sm font-medium\">{heading}</h3>\n                  </li>\n                )}\n                <TimestampTooltipGroup active={Boolean(onRowClick)}>\n                  <li\n                    data-state={row.getIsSelected() ? \"selected\" : undefined}\n                    className=\"min-w-0 space-y-2 p-3 data-[state=selected]:bg-muted\"\n                  >\n                    <div className=\"flex min-w-0 items-center justify-between gap-3\">\n                      <div className=\"min-w-0 flex-1\">{content.primary}</div>\n                      {content.status}\n                      {renderExpanded && (\n                        <DataTableExpandButton\n                          open={open}\n                          label={rowLabel}\n                          previewId={previewId}\n                          onToggle={() => row.toggleExpanded()}\n                        />\n                      )}\n                      {onRowClick && (\n                        <DataTableOpenButton\n                          timestampTarget\n                          inspect={endAction}\n                          label={rowLabel}\n                          onOpen={() => onRowClick(row.original)}\n                        />\n                      )}\n                    </div>\n                    <div className=\"min-w-0 truncate text-sm text-muted-foreground\">\n                      {content.secondary}\n                    </div>\n                    {renderExpanded && open && (\n                      <div id={previewId} className=\"bg-muted/40 p-3\">\n                        {renderExpanded(row.original)}\n                      </div>\n                    )}\n                  </li>\n                </TimestampTooltipGroup>\n              </Fragment>\n            );\n          })}\n        </ul>\n      ) : (\n        <Table\n          containerClassName={cn(\"rounded-lg\", scrollable && \"max-h-[70dvh] overflow-y-auto\")}\n          aria-label={label}\n          className={className}\n        >\n          <TableHeader className={cn(\"bg-muted\", scrollable && \"sticky top-0 z-10\")}>\n            {table.getHeaderGroups().map((group) => (\n              <TableRow key={group.id}>\n                {renderExpanded ? (\n                  <TableHead scope=\"col\" className=\"h-8 w-8 px-1\">\n                    <span className=\"sr-only\">Expand</span>\n                  </TableHead>\n                ) : null}\n                {group.headers.map((header) => {\n                  const column = columns.find((entry) => entry.id === header.column.id);\n                  const direction = header.column.getIsSorted();\n                  return (\n                    <TableHead\n                      key={header.id}\n                      scope=\"col\"\n                      aria-sort={\n                        direction === \"asc\"\n                          ? \"ascending\"\n                          : direction === \"desc\"\n                            ? \"descending\"\n                            : column?.sortable\n                              ? \"none\"\n                              : undefined\n                      }\n                      className={cn(\n                        \"h-10 px-3\",\n                        density === \"compact\" && \"h-8 px-2\",\n                        column?.headerClassName,\n                      )}\n                    >\n                      <span className=\"inline-flex items-center gap-1\">\n                        {column?.sortable && onSort ? (\n                          <button\n                            type=\"button\"\n                            className={cn(\n                              \"inline-flex min-h-8 items-center gap-1 whitespace-nowrap rounded-sm font-[inherit] tracking-[inherit] [text-transform:inherit] hover:text-foreground\",\n                              focusRing,\n                            )}\n                            onClick={() => onSort(column.id)}\n                            aria-label={`Sort by ${column.header}`}\n                          >\n                            {column.header}\n                            {direction === \"desc\" ? (\n                              <ChevronDown aria-hidden className=\"size-3\" />\n                            ) : direction === \"asc\" ? (\n                              <ChevronUp aria-hidden className=\"size-3\" />\n                            ) : (\n                              <ArrowUpDown aria-hidden className=\"size-3 opacity-50\" />\n                            )}\n                          </button>\n                        ) : (\n                          flexRender(header.column.columnDef.header, header.getContext())\n                        )}\n                        {column?.description && <DataTableColumnLabel column={column} iconOnly />}\n                      </span>\n                    </TableHead>\n                  );\n                })}\n                {endAction && (\n                  <TableHead scope=\"col\">\n                    <span className=\"sr-only\">Inspect</span>\n                  </TableHead>\n                )}\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {plainEmpty && (\n              <TableRow>\n                <TableCell colSpan={span} className=\"p-3 text-muted-foreground\">\n                  {emptyState}\n                </TableCell>\n              </TableRow>\n            )}\n            {rows.map((row, index) => {\n              const open = row.getIsExpanded();\n              const previewId = `${instanceId}-preview-${row.id}`;\n              const heading = groupHeading(index);\n              return (\n                <Fragment key={row.id}>\n                  {heading !== undefined && (\n                    <TableRow>\n                      <TableHead\n                        scope=\"colgroup\"\n                        colSpan={span}\n                        className=\"h-9 bg-muted/40 px-3 text-sm font-medium normal-case tracking-normal text-foreground\"\n                      >\n                        {heading}\n                      </TableHead>\n                    </TableRow>\n                  )}\n                  <TimestampTooltipGroup active={Boolean(onRowClick)}>\n                    <TimestampTooltipTarget>\n                      <TableRow\n                        data-state={row.getIsSelected() ? \"selected\" : undefined}\n                        data-interactive={onRowClick ? \"true\" : undefined}\n                        className={\n                          onRowClick\n                            ? cn(\n                                \"cursor-pointer focus-within:outline-2 focus-within:outline-ring focus-within:outline-offset-[-2px]\",\n                                focusRing,\n                              )\n                            : undefined\n                        }\n                        tabIndex={onRowClick ? 0 : undefined}\n                        onClick={\n                          onRowClick\n                            ? (event) => {\n                                if (\n                                  !(event.target instanceof Element) ||\n                                  !event.target.closest(\"button, a, input, select, textarea\")\n                                ) {\n                                  onRowClick(row.original);\n                                }\n                              }\n                            : undefined\n                        }\n                        onKeyDown={\n                          onRowClick\n                            ? (event) => {\n                                if (\n                                  event.target === event.currentTarget &&\n                                  (event.key === \"Enter\" || event.key === \" \")\n                                ) {\n                                  event.preventDefault();\n                                  onRowClick(row.original);\n                                }\n                              }\n                            : undefined\n                        }\n                      >\n                        {renderExpanded ? (\n                          <TableCell className=\"w-8 px-1 py-1\">\n                            <DataTableExpandButton\n                              open={open}\n                              label={getRowLabel?.(row.original) ?? row.id}\n                              previewId={previewId}\n                              onToggle={() => row.toggleExpanded()}\n                            />\n                          </TableCell>\n                        ) : null}\n                        {row.getVisibleCells().map((cell, index) => (\n                          <TableCell\n                            key={cell.id}\n                            className={cn(\n                              density === \"compact\" ? \"px-2 py-1\" : \"px-3 py-3\",\n                              columns.find((entry) => entry.id === cell.column.id)?.cellClassName,\n                            )}\n                          >\n                            {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                            {onRowClick && !endAction && index === 0 && (\n                              <DataTableOpenButton\n                                tabIndex={-1}\n                                label={getRowLabel?.(row.original) ?? row.id}\n                                onOpen={() => onRowClick(row.original)}\n                              />\n                            )}\n                          </TableCell>\n                        ))}\n                        {endAction && onRowClick && (\n                          <TableCell className=\"w-px px-3 text-right\">\n                            <DataTableOpenButton\n                              tabIndex={-1}\n                              inspect\n                              label={getRowLabel?.(row.original) ?? row.id}\n                              onOpen={() => onRowClick(row.original)}\n                            />\n                          </TableCell>\n                        )}\n                      </TableRow>\n                    </TimestampTooltipTarget>\n                  </TimestampTooltipGroup>\n                  {renderExpanded && open ? (\n                    <TableRow>\n                      <TableCell colSpan={span} className=\"bg-muted/40 px-3 py-2\">\n                        <div id={previewId}>{renderExpanded(row.original)}</div>\n                      </TableCell>\n                    </TableRow>\n                  ) : null}\n                </Fragment>\n              );\n            })}\n          </TableBody>\n        </Table>\n      )}\n    </div>\n  );\n}\n\nfunction DataTableExpandButton({\n  open,\n  label,\n  previewId,\n  onToggle,\n}: {\n  open: boolean;\n  label: string;\n  previewId: string;\n  onToggle: () => void;\n}) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={`${open ? \"Collapse\" : \"Expand\"} ${label}`}\n      aria-expanded={open}\n      aria-controls={open ? previewId : undefined}\n      className={cn(\n        \"flex size-7 shrink-0 items-center justify-center rounded-sm hover:bg-muted\",\n        focusRing,\n      )}\n      onClick={onToggle}\n    >\n      <ChevronRight\n        aria-hidden\n        className={cn(\n          \"size-4 transition-transform motion-reduce:transition-none\",\n          open && \"rotate-90\",\n        )}\n      />\n    </button>\n  );\n}\nfunction DataTableOpenButton({\n  label,\n  onOpen,\n  inspect = false,\n  tabIndex,\n  timestampTarget = false,\n}: {\n  label: string;\n  onOpen: () => void;\n  inspect?: boolean;\n  tabIndex?: number;\n  timestampTarget?: boolean;\n}) {\n  const button = inspect ? (\n    <Button\n      variant=\"ghost\"\n      size=\"sm\"\n      aria-label={`Inspect ${label}`}\n      onClick={onOpen}\n      tabIndex={tabIndex}\n    >\n      Inspect\n    </Button>\n  ) : (\n    <button\n      type=\"button\"\n      aria-label={`Open ${label}`}\n      onClick={onOpen}\n      tabIndex={tabIndex}\n      className={cn(\n        \"ml-2 inline-flex size-7 shrink-0 items-center justify-center rounded-sm align-middle hover:bg-muted\",\n        focusRing,\n      )}\n    >\n      <ChevronRight aria-hidden className=\"size-4\" />\n    </button>\n  );\n  return timestampTarget ? <TimestampTooltipTarget>{button}</TimestampTooltipTarget> : button;\n}\n\nfunction DataTableColumnLabel({\n  column,\n  content,\n  iconOnly = false,\n}: {\n  column: { header: string; description?: string };\n  content?: ReactNode;\n  iconOnly?: boolean;\n}) {\n  const id = useId();\n  if (!column.description) {\n    return <span>{content ?? column.header}</span>;\n  }\n  const help = (\n    <Tooltip>\n      <TooltipTrigger\n        aria-label={`About ${column.header}`}\n        aria-describedby={id}\n        render={\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"sm\"\n            className={cn(\n              \"min-h-6 gap-1 p-0 font-normal normal-case tracking-normal\",\n              iconOnly || content !== undefined ? \"size-8 shrink-0\" : \"h-auto\",\n            )}\n          />\n        }\n      >\n        {iconOnly || content !== undefined ? (\n          <Info aria-hidden className=\"size-3.5\" />\n        ) : (\n          <>\n            <span>{column.header}</span>\n            <Info aria-hidden className=\"size-3.5\" />\n          </>\n        )}\n      </TooltipTrigger>\n      <TooltipContent id={id} role=\"tooltip\">\n        {column.description}\n      </TooltipContent>\n    </Tooltip>\n  );\n  return content === undefined ? (\n    help\n  ) : (\n    <span className=\"inline-flex min-w-0 items-center gap-1\">\n      {content}\n      {help}\n    </span>\n  );\n}\n\nfunction mobileLabels<T>(columns: readonly DataTableColumn<T>[]): DataTableMobileLabels {\n  return {\n    label: (id, content) => {\n      const column = columns.find((entry) => entry.id === id);\n      return column ? <DataTableColumnLabel column={column} content={content} /> : null;\n    },\n  };\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table.tsx"
    }
  ],
  "type": "registry:block"
}