{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "openlmis-data-table",
  "title": "Data Table",
  "description": "Server-paged table on TanStack Table v9: sortable headers, fixed column widths that hold steady from page to page, columns that drop when the table is narrow, a skeleton built from the real columns, empty and error states, and a pagination footer.",
  "dependencies": [
    "@tanstack/react-table@^9",
    "lucide-react@^1",
    "@base-ui/react@^1"
  ],
  "registryDependencies": [
    "button",
    "empty",
    "skeleton",
    "table",
    "https://registry.soldevelo.com/r/openlmis-pagination.json"
  ],
  "files": [
    {
      "path": "registry/blocks/openlmis/data-table/data-table.tsx",
      "content": "\"use client\"\n\nimport {\n  type Column,\n  columnVisibilityFeature,\n  FlexRender,\n  type ReactTable,\n  type RowData,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  rowSortingFeature,\n  tableFeatures,\n} from \"@tanstack/react-table\"\nimport {\n  AlertTriangleIcon,\n  ChevronDownIcon,\n  ChevronsUpDownIcon,\n  ChevronUpIcon,\n} from \"lucide-react\"\nimport type { ReactNode } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Empty,\n  EmptyContent,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/components/ui/empty\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\"\nimport {\n  Pagination,\n  type PaginationLabels,\n  PaginationSkeleton,\n} from \"@/components/openlmis/pagination\"\n\nexport type DataTableColumnMeta = {\n  /** Width classes, e.g. `w-1/5` or `w-16 @xl/table:w-32`. Columns without any share what is left. */\n  className?: string\n  /** The loading placeholder's size: `text` (default) is a bar, `checkbox` a small square. */\n  skeleton?: \"text\" | \"checkbox\"\n}\n\n// Sorting and paging are left to the data source, so no client row models are registered.\nexport const dataTableFeatures = tableFeatures({\n  columnVisibilityFeature,\n  rowSortingFeature,\n  rowPaginationFeature,\n  rowSelectionFeature,\n  columnMeta: {} as DataTableColumnMeta,\n})\n\nexport type DataTableFeatures = typeof dataTableFeatures\n\nexport type DataTableInstance<TData extends RowData> = ReactTable<\n  DataTableFeatures,\n  TData\n>\n\n// Fixed layout keeps widths still from page to page.\nconst TABLE_CLASSES =\n  \"table-fixed [&_td]:h-12 [&_td]:px-4 [&_td]:py-1.5 [&_th]:h-10 [&_th]:px-4\"\n\ntype DataTableProps<TData extends RowData> = {\n  table: DataTableInstance<TData>\n  /** Rendered across the whole body when there are no rows. */\n  empty?: ReactNode\n  /** Dims the rows while the next page is loading in the background. */\n  isStale?: boolean\n  /** Rendered inside the card below the rows, e.g. `DataTablePagination`. */\n  footer?: ReactNode\n}\n\nexport function DataTable<TData extends RowData>({\n  table,\n  empty,\n  isStale = false,\n  footer,\n}: DataTableProps<TData>) {\n  const rows = table.getRowModel().rows\n  const columns = table.getVisibleLeafColumns()\n\n  return (\n    <DataTableCard>\n      <div\n        aria-busy={isStale}\n        className=\"transition-opacity aria-busy:opacity-60\"\n      >\n        <Table className={TABLE_CLASSES}>\n          <DataTableHeader table={table} />\n          <TableBody>\n            {rows.length > 0 ? (\n              rows.map((row) => (\n                <TableRow\n                  data-state={row.getIsSelected() ? \"selected\" : undefined}\n                  key={row.id}\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <TableCell key={cell.id}>\n                      {/* Cuts overflow with an ellipsis; the padding keeps a button's focus ring inside. */}\n                      <div className=\"-m-1 truncate p-1\">\n                        <FlexRender cell={cell} />\n                      </div>\n                    </TableCell>\n                  ))}\n                </TableRow>\n              ))\n            ) : (\n              <TableRow>\n                <TableCell colSpan={columns.length}>\n                  <div className=\"whitespace-normal\">{empty}</div>\n                </TableCell>\n              </TableRow>\n            )}\n          </TableBody>\n        </Table>\n      </div>\n      {footer && <DataTableFooter>{footer}</DataTableFooter>}\n    </DataTableCard>\n  )\n}\n\n/** Column widths and the header row, shared by the table and its skeleton so neither shifts. */\nfunction DataTableHeader<TData extends RowData>({\n  table,\n}: {\n  table: DataTableInstance<TData>\n}) {\n  return (\n    <>\n      <colgroup>\n        {table.getVisibleLeafColumns().map((column) => (\n          <col className={column.columnDef.meta?.className} key={column.id} />\n        ))}\n      </colgroup>\n      <TableHeader>\n        {table.getHeaderGroups().map((headerGroup) => (\n          <TableRow key={headerGroup.id}>\n            {headerGroup.headers.map((header) => (\n              <TableHead aria-sort={ariaSort(header.column)} key={header.id}>\n                {header.isPlaceholder ? null : <FlexRender header={header} />}\n              </TableHead>\n            ))}\n          </TableRow>\n        ))}\n      </TableHeader>\n    </>\n  )\n}\n\n// A size container, so the footer and columns lay out by the table's width rather than the window's.\nexport function DataTableCard({ children }: { children: ReactNode }) {\n  return (\n    <div className=\"@container/table overflow-hidden rounded-xl border bg-card shadow-xs [&_thead]:bg-muted/50 [&_thead_tr]:hover:bg-transparent\">\n      {children}\n    </div>\n  )\n}\n\nfunction DataTableFooter({ children }: { children: ReactNode }) {\n  return <div className=\"border-t bg-muted/30 px-4 py-3\">{children}</div>\n}\n\nfunction ariaSort<TData extends RowData>(\n  column: Column<DataTableFeatures, TData>\n) {\n  if (!column.getCanSort()) return undefined\n  const direction = column.getIsSorted()\n  if (direction === \"asc\") return \"ascending\"\n  if (direction === \"desc\") return \"descending\"\n  return \"none\"\n}\n\ntype DataTableColumnHeaderProps<TData extends RowData, TValue> = {\n  column: Column<DataTableFeatures, TData, TValue>\n  title: string\n}\n\n/** Header label that toggles the column's sort when the column allows it. */\nexport function DataTableColumnHeader<TData extends RowData, TValue>({\n  column,\n  title,\n}: DataTableColumnHeaderProps<TData, TValue>) {\n  if (!column.getCanSort())\n    return <DataTableHeaderLabel>{title}</DataTableHeaderLabel>\n\n  const direction = column.getIsSorted()\n  const SortIcon =\n    direction === \"asc\"\n      ? ChevronUpIcon\n      : direction === \"desc\"\n        ? ChevronDownIcon\n        : ChevronsUpDownIcon\n\n  return (\n    <div className=\"-ms-2\">\n      <Button\n        onClick={column.getToggleSortingHandler()}\n        size=\"xs\"\n        variant=\"ghost\"\n      >\n        <DataTableHeaderLabel>{title}</DataTableHeaderLabel>\n        <SortIcon className=\"text-muted-foreground\" data-icon=\"inline-end\" />\n      </Button>\n    </div>\n  )\n}\n\nexport function DataTableHeaderLabel({ children }: { children: ReactNode }) {\n  return (\n    <span className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">\n      {children}\n    </span>\n  )\n}\n\nconst PAGINATION_SKELETON = <PaginationSkeleton />\n\ntype DataTableSkeletonProps<TData extends RowData> = {\n  /** A table built from the real columns with no rows, so the header and widths match exactly. */\n  table: DataTableInstance<TData>\n  rowCount: number\n  footer?: ReactNode\n}\n\nexport function DataTableSkeleton<TData extends RowData>({\n  table,\n  rowCount,\n  footer = PAGINATION_SKELETON,\n}: DataTableSkeletonProps<TData>) {\n  const columns = table.getVisibleLeafColumns()\n  const rows = Array.from({ length: rowCount }, (_, index) => index)\n\n  return (\n    <div aria-busy>\n      <DataTableCard>\n        <Table className={TABLE_CLASSES}>\n          <DataTableHeader table={table} />\n          <TableBody>\n            {rows.map((row) => (\n              <TableRow key={row}>\n                {columns.map((column) => (\n                  <TableCell key={column.id}>\n                    <Skeleton\n                      className={\n                        column.columnDef.meta?.skeleton === \"checkbox\"\n                          ? \"size-4\"\n                          : \"h-4 w-3/4\"\n                      }\n                    />\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n        {footer && <DataTableFooter>{footer}</DataTableFooter>}\n      </DataTableCard>\n    </div>\n  )\n}\n\ntype DataTableEmptyProps = {\n  icon?: ReactNode\n  title: string\n  description?: string | undefined\n  action?: ReactNode\n}\n\nexport function DataTableEmpty({\n  icon,\n  title,\n  description,\n  action,\n}: DataTableEmptyProps) {\n  return (\n    <Empty>\n      <EmptyHeader>\n        {icon && <EmptyMedia variant=\"icon\">{icon}</EmptyMedia>}\n        <EmptyTitle>{title}</EmptyTitle>\n        {description && <EmptyDescription>{description}</EmptyDescription>}\n      </EmptyHeader>\n      {action && <EmptyContent>{action}</EmptyContent>}\n    </Empty>\n  )\n}\n\ntype DataTableErrorProps = {\n  title: string\n  description?: string\n  onRetry?: () => void\n  retryLabel?: string\n  /** Replaces the warning triangle, e.g. a wifi-off icon for an offline failure. */\n  icon?: ReactNode\n}\n\nexport function DataTableError({\n  title,\n  description,\n  onRetry,\n  retryLabel = \"Try Again\",\n  icon,\n}: DataTableErrorProps) {\n  return (\n    <DataTableCard>\n      <DataTableEmpty\n        action={\n          onRetry && (\n            <Button onClick={onRetry} size=\"sm\" variant=\"outline\">\n              {retryLabel}\n            </Button>\n          )\n        }\n        description={description}\n        icon={icon ?? <AlertTriangleIcon />}\n        title={title}\n      />\n    </DataTableCard>\n  )\n}\n\ntype DataTablePaginationProps<TData extends RowData> = {\n  table: DataTableInstance<TData>\n  labels?: Partial<PaginationLabels>\n}\n\n/** The table's pagination state wired to the Pagination component, for the table's footer. */\nexport function DataTablePagination<TData extends RowData>({\n  table,\n  labels,\n}: DataTablePaginationProps<TData>) {\n  // Subscribed here, so it still updates when the caller's useTable selector leaves pagination out.\n  return (\n    <table.Subscribe selector={(state) => state.pagination}>\n      {({ pageIndex, pageSize }) => (\n        <Pagination\n          labels={labels}\n          onPageChange={table.setPageIndex}\n          onPageSizeChange={table.setPageSize}\n          pageIndex={pageIndex}\n          pageSize={pageSize}\n          rowCount={table.getRowCount()}\n        />\n      )}\n    </table.Subscribe>\n  )\n}",
      "type": "registry:component",
      "target": "components/blocks/openlmis/data-table/data-table.tsx"
    },
    {
      "path": "registry/blocks/openlmis/data-table/responsive-columns.ts",
      "content": "\"use client\"\n\nimport type { ColumnVisibilityState } from \"@tanstack/react-table\"\nimport { useCallback, useRef, useState } from \"react\"\n\n/** Tailwind's container sizes in px, smallest first, so defaults switch where `@md:`-style classes do. */\nconst CONTAINER_WIDTHS = {\n  sm: 384,\n  md: 448,\n  lg: 512,\n  xl: 576,\n  \"2xl\": 672,\n  \"3xl\": 768,\n  \"4xl\": 896,\n  \"5xl\": 1024,\n} as const\n\nexport type ContainerSize = keyof typeof CONTAINER_WIDTHS\n\nconst SIZES = Object.keys(CONTAINER_WIDTHS) as ContainerSize[]\n\nexport type ResponsiveColumn = {\n  id: string\n  /** The column is hidden by default when the table has less room than this container size. */\n  hideBelow?: ContainerSize\n}\n\n/** The largest container size that fits, or \"none\" when even the smallest does not. */\ntype FittingSize = ContainerSize | \"none\"\n\nfunction fittingSize(width: number): FittingSize {\n  let fitting: FittingSize = \"none\"\n  for (const size of SIZES) if (width >= CONTAINER_WIDTHS[size]) fitting = size\n  return fitting\n}\n\n/** The room an element has, as a container size; a sidebar changes it, not just the window. */\nexport function useContainerSize<T extends HTMLElement>() {\n  // A size rather than a width, so resizing only re-renders when a size boundary is crossed.\n  const [size, setSize] = useState<FittingSize | undefined>(undefined)\n  const observer = useRef<ResizeObserver | null>(null)\n\n  // A ref callback measures during commit, so the first size is in before the browser paints.\n  const ref = useCallback((element: T | null) => {\n    observer.current?.disconnect()\n    observer.current = null\n    if (!element) return\n    setSize(fittingSize(element.getBoundingClientRect().width))\n    observer.current = new ResizeObserver(([entry]) => {\n      // The border box, like getBoundingClientRect, so padding never shifts the result.\n      const width = entry?.borderBoxSize[0]?.inlineSize\n      if (width !== undefined) setSize(fittingSize(width))\n    })\n    observer.current.observe(element)\n  }, [])\n\n  return [ref, size] as const\n}\n\nfunction fits(size: FittingSize | undefined, needed: ContainerSize) {\n  // Before the first measurement everything counts as fitting; the ref callback corrects it before paint.\n  if (size === undefined) return true\n  return size !== \"none\" && SIZES.indexOf(size) >= SIZES.indexOf(needed)\n}\n\n/** Visibility that fits the room until the user picks; the caller stores the picks and measures the room. */\nexport function useColumnVisibility(\n  columns: readonly ResponsiveColumn[],\n  [choices, setChoices]: readonly [\n    ColumnVisibilityState,\n    (next: ColumnVisibilityState) => void,\n  ],\n  size: FittingSize | undefined\n) {\n  const visibility: ColumnVisibilityState = Object.fromEntries(\n    columns.map((column) => [\n      column.id,\n      choices[column.id] ??\n        (column.hideBelow ? fits(size, column.hideBelow) : true),\n    ])\n  )\n\n  return {\n    visibility,\n    // Only the columns the user just toggled are stored, so the rest keep following the room.\n    onVisibilityChange: (next: ColumnVisibilityState) =>\n      setChoices({\n        ...choices,\n        ...Object.fromEntries(\n          Object.entries(next).filter(([id, shown]) => visibility[id] !== shown)\n        ),\n      }),\n    onReset: () => setChoices({}),\n  }\n}",
      "type": "registry:lib",
      "target": "components/blocks/openlmis/data-table/responsive-columns.ts"
    },
    {
      "path": "registry/blocks/openlmis/data-table/row-actions.ts",
      "content": "\"use client\"\n\nimport { useCallback, useRef } from \"react\"\n\n/** Keeps a closing menu from taking focus back to its trigger when an item opened a dialog, which then owns focus. */\nexport function useMenuOpensDialog() {\n  const opened = useRef(false)\n\n  const onOpenChange = useCallback((open: boolean) => {\n    if (open) opened.current = false\n  }, [])\n  const finalFocus = useCallback(() => !opened.current, [])\n  const opensDialog = useCallback(\n    (open: () => void) => () => {\n      opened.current = true\n      open()\n    },\n    []\n  )\n\n  return { onOpenChange, finalFocus, opensDialog }\n}",
      "type": "registry:lib",
      "target": "components/blocks/openlmis/data-table/row-actions.ts"
    }
  ],
  "cssVars": {
    "light": {
      "destructive": "oklch(0.577 0.245 27.325)",
      "destructive-foreground": "oklch(0.985 0 0)",
      "success": "oklch(0.596 0.145 163.225)",
      "success-foreground": "oklch(0.985 0 0)",
      "warning": "oklch(0.666 0.179 58.318)",
      "warning-foreground": "oklch(0.985 0 0)",
      "info": "oklch(0.55 0.13 237)",
      "info-foreground": "oklch(0.985 0 0)"
    },
    "dark": {
      "destructive": "oklch(0.704 0.191 22.216)",
      "destructive-foreground": "oklch(0.21 0.04 25)",
      "success": "oklch(0.696 0.17 162.48)",
      "success-foreground": "oklch(0.21 0.04 163)",
      "warning": "oklch(0.769 0.188 70.08)",
      "warning-foreground": "oklch(0.22 0.04 60)",
      "info": "oklch(0.7 0.14 237)",
      "info-foreground": "oklch(0.2 0.04 240)"
    }
  },
  "meta": {
    "project": "openlmis",
    "height": "639px"
  },
  "categories": [
    "data-table"
  ],
  "type": "registry:block"
}
