{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "description": "Hand it rows and columns and get sorting, a filter box, selection, column visibility, pages, loading rows and bulk actions, with rows that turn into cards in a narrow container.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/table.json",
    "https://miseui.junsu4621.workers.dev/r/checkbox.json",
    "https://miseui.junsu4621.workers.dev/r/pagination.json",
    "https://miseui.junsu4621.workers.dev/r/popover.json",
    "https://miseui.junsu4621.workers.dev/r/input.json",
    "https://miseui.junsu4621.workers.dev/r/button.json",
    "https://miseui.junsu4621.workers.dev/r/skeleton.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/data-table.tsx",
      "content": "import type * as React from \"react\"\nimport { filterRows, type DataTableColumn } from \"@/registry/miseui/ui/data-table-math\"\nimport { useDataTable, type DataTableState, type DataTableStateOptions } from \"@/registry/miseui/ui/data-table-state\"\nimport { DataTableToolbar, type DataTableLabels } from \"@/registry/miseui/ui/data-table-toolbar\"\nimport { DataTableCards, DataTableTable } from \"@/registry/miseui/ui/data-table-views\"\nimport { Pagination } from \"@/registry/miseui/ui/pagination\"\nimport type { TableVariant } from \"@/registry/miseui/ui/table\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype DataTableVariant = \"plain\" | \"card\" | \"compact\" | \"cards\"\ntype DataTableEffect = \"spring\"\n\nconst DEFAULT_PAGE_SIZE = 10\nconst DEFAULT_SKELETON_ROWS = 5\n\nconst DEFAULT_LABELS: DataTableLabels = {\n  filter: \"Filter rows\",\n  columns: \"Show or hide columns\",\n  sort: \"Sort rows\",\n  unsorted: \"Sort\",\n  ascending: \"ascending\",\n  descending: \"descending\",\n  clearSelection: \"Clear\",\n  clearFilter: \"Clear filter\",\n  selectPage: \"Select all on this page\",\n  selectRow: (row) => `Select ${row}`,\n  selectedCount: (count) => `${count} selected`,\n  range: (from, to, total) => (total === 0 ? \"No rows\" : `${from} to ${to} of ${total}`),\n  noMatch: (query) => `No rows match \"${query}\".`,\n  loading: \"Loading rows\",\n}\n\nconst TABLE_VARIANT: Record<DataTableVariant, TableVariant> = { plain: \"plain\", card: \"card\", compact: \"compact\", cards: \"plain\" }\n\ninterface DataTableProps<Row> extends Omit<DataTableStateOptions<Row>, \"pageSize\" | \"getRowId\">, Omit<React.ComponentProps<\"div\">, \"children\" | \"defaultValue\"> {\n  /** Names the table for assistive tech. */\n  label: string\n  /** Without it, the row's `id` field. */\n  getRowId?: (row: Row) => string\n  pageSize?: number\n  /** `plain` hairlines on the page, `card` one framed surface, `compact` dense rows for scanning, `cards` turns rows into cards in a narrow container. */\n  variant?: DataTableVariant\n  /** `spring` slides one highlight between rows as the pointer or focus moves. */\n  effect?: DataTableEffect\n  /** Adds checkboxes. */\n  selectable?: boolean\n  /** Replaces the filter row while rows are selected. Gets every selected row, on any page. */\n  bulkActions?: (rows: readonly Row[], clear: () => void) => React.ReactNode\n  /** Extra controls beside the filter. */\n  actions?: React.ReactNode\n  loading?: boolean\n  /** Skeleton rows while loading. */\n  loadingRows?: number\n  /** Shown when there is no data at all. A filter with no match gets its own message and a way out. */\n  emptyText?: string\n  labels?: Partial<DataTableLabels>\n}\n\nconst readId = <Row,>(row: Row) => String((row as { id?: unknown }).id)\n\nfunction Footer<Row>({ table, labels, loading, className }: { table: DataTableState<Row>; labels: DataTableLabels; loading: boolean; className?: string }) {\n  // While loading or empty the body already says so, and a count under it would only contradict the skeleton.\n  if (loading || table.total === 0) return null\n  return (\n    <div data-slot=\"data-table-footer\" className={cn(\"flex flex-wrap items-center justify-between gap-2\", className)}>\n      <p className=\"text-[13px] text-muted-foreground tabular-nums\">{labels.range(table.range.from, table.range.to, table.total)}</p>\n      {table.pageCount > 1 ? <Pagination variant=\"simple\" page={table.page} pageCount={table.pageCount} onPageChange={table.setPage} /> : null}\n    </div>\n  )\n}\n\nfunction DataTable<Row>({\n  label,\n  rows,\n  columns,\n  getRowId = readId,\n  pageSize = DEFAULT_PAGE_SIZE,\n  variant = \"plain\",\n  effect,\n  selectable = false,\n  bulkActions,\n  actions,\n  loading = false,\n  loadingRows = DEFAULT_SKELETON_ROWS,\n  emptyText = \"Nothing here yet.\",\n  labels: labelOverrides,\n  className,\n  defaultSort, locale, query, defaultQuery, onQueryChange, selected, defaultSelected, onSelectedChange, hiddenColumns, defaultHiddenColumns, onHiddenColumnsChange,\n  ...props\n}: DataTableProps<Row>) {\n  const stateOptions = { defaultSort, locale, query, defaultQuery, onQueryChange, selected, defaultSelected, onSelectedChange, hiddenColumns, defaultHiddenColumns, onHiddenColumnsChange }\n  const table = useDataTable({ rows, columns, getRowId, pageSize, ...stateOptions })\n  const labels = { ...DEFAULT_LABELS, ...labelOverrides }\n  const framed = variant === \"card\"\n  const cards = variant === \"cards\"\n  const view = { table, labels, selectable, loading, loadingRows, emptyText }\n  const tableView = (\n    <DataTableTable {...view} variant={TABLE_VARIANT[variant]} effect={effect} label={label} className={cn(framed && \"rounded-none border-0 bg-transparent\", cards && \"hidden @md:block\")} />\n  )\n  return (\n    <div\n      data-slot=\"data-table\"\n      data-variant={variant}\n      data-effect={effect}\n      aria-busy={loading || undefined}\n      className={cn(\"@container grid w-full min-w-0 grid-cols-[minmax(0,1fr)]\", framed ? \"rounded-lg border bg-background\" : \"gap-3\", className)}\n      {...props}\n    >\n      <DataTableToolbar table={table} labels={labels} compact={variant === \"compact\"} cards={cards} bulkActions={bulkActions} actions={actions} className={cn(framed && \"border-b p-3\")} />\n      {tableView}\n      {cards ? <DataTableCards {...view} className=\"@md:hidden\" /> : null}\n      <Footer table={table} labels={labels} loading={loading} className={cn(framed && \"border-t px-3 py-2\")} />\n      {loading ? (\n        <span role=\"status\" className=\"sr-only\">\n          {labels.loading}\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\nexport { DataTable, filterRows, type DataTableColumn, type DataTableEffect, type DataTableLabels, type DataTableProps, type DataTableVariant }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/data-table-math.ts",
      "content": "import type * as React from \"react\"\nimport type { SortAccessor, SortDirection, TableSort } from \"@/registry/miseui/ui/table-state\"\n\ninterface DataTableColumn<Row> {\n  key: string\n  header: string\n  /** What sorting and filtering read. Without it, the field of the row named by `key`. */\n  value?: SortAccessor<Row>\n  /** What the cell shows. Without it, the value as text. */\n  cell?: (row: Row) => React.ReactNode\n  /** Defaults to true. */\n  sortable?: boolean\n  /** Defaults to true. Turn off for ids and figures nobody searches by. */\n  filterable?: boolean\n  /** Defaults to true. False keeps the column on the screen and out of the column menu's reach. */\n  hideable?: boolean\n  /** Right-aligned tabular figures. */\n  numeric?: boolean\n  className?: string\n}\n\nfunction columnValue<Row>(column: DataTableColumn<Row>, row: Row) {\n  if (column.value) return column.value(row)\n  return (row as Record<string, unknown>)[column.key] as ReturnType<SortAccessor<Row>>\n}\n\nfunction columnText<Row>(column: DataTableColumn<Row>, row: Row) {\n  const value = columnValue(column, row)\n  if (value === null || value === undefined) return \"\"\n  return value instanceof Date ? value.toLocaleDateString() : String(value)\n}\n\n/** Every word has to appear somewhere in the row, so \"north bakery\" finds a row whose words sit in two columns. */\nfunction filterRows<Row>(rows: readonly Row[], columns: readonly DataTableColumn<Row>[], query: string) {\n  const terms = query.trim().toLocaleLowerCase().split(/\\s+/).filter(Boolean)\n  if (terms.length === 0) return rows\n  const searchable = columns.filter((column) => column.filterable !== false)\n  return rows.filter((row) => {\n    const haystack = searchable.map((column) => columnText(column, row)).join(\" \").toLocaleLowerCase()\n    return terms.every((term) => haystack.includes(term))\n  })\n}\n\nconst pageCountOf = (total: number, pageSize: number) => Math.max(1, Math.ceil(total / Math.max(1, pageSize)))\n\nfunction pageSlice<Row>(rows: readonly Row[], page: number, pageSize: number) {\n  const start = (page - 1) * pageSize\n  return rows.slice(start, start + pageSize)\n}\n\nfunction pageRange(page: number, pageSize: number, total: number) {\n  if (total === 0) return { from: 0, to: 0 }\n  return { from: (page - 1) * pageSize + 1, to: Math.min(total, page * pageSize) }\n}\n\nfunction toggleId(selected: readonly string[], id: string) {\n  return selected.includes(id) ? selected.filter((entry) => entry !== id) : [...selected, id]\n}\n\n/** All on leaves the page empty; anything less fills it. Rows chosen on other pages are not touched. */\nfunction togglePage(selected: readonly string[], ids: readonly string[]) {\n  const allOn = ids.length > 0 && ids.every((id) => selected.includes(id))\n  if (allOn) return selected.filter((id) => !ids.includes(id))\n  return [...selected, ...ids.filter((id) => !selected.includes(id))]\n}\n\nfunction pageChecked(selected: readonly string[], ids: readonly string[]) {\n  const on = ids.filter((id) => selected.includes(id)).length\n  if (on === 0) return false\n  return on === ids.length ? true : (\"indeterminate\" as const)\n}\n\n/** The last visible column stays: a table with no columns has no way back to the menu's meaning. */\nfunction toggleColumn(hidden: readonly string[], key: string, allKeys: readonly string[]) {\n  if (hidden.includes(key)) return hidden.filter((entry) => entry !== key)\n  const visible = allKeys.filter((entry) => !hidden.includes(entry))\n  return visible.length <= 1 ? hidden : [...hidden, key]\n}\n\nfunction sortToOption<Key extends string>(sort: TableSort<Key> | null) {\n  return sort ? `${sort.key}:${sort.direction}` : \"\"\n}\n\nfunction sortFromOption(option: string): TableSort<string> | null {\n  const [key, direction] = option.split(\":\")\n  if (!key || (direction !== \"ascending\" && direction !== \"descending\")) return null\n  return { key, direction: direction as SortDirection }\n}\n\nexport {\n  columnText,\n  columnValue,\n  filterRows,\n  pageChecked,\n  pageCountOf,\n  pageRange,\n  pageSlice,\n  sortFromOption,\n  sortToOption,\n  toggleColumn,\n  toggleId,\n  togglePage,\n  type DataTableColumn,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/data-table-state.ts",
      "content": "import { useMemo, useState } from \"react\"\r\nimport {\r\n  columnValue,\r\n  filterRows,\r\n  pageChecked,\r\n  pageCountOf,\r\n  pageRange,\r\n  pageSlice,\r\n  toggleColumn,\r\n  toggleId,\r\n  togglePage,\r\n  type DataTableColumn,\r\n} from \"@/registry/miseui/ui/data-table-math\"\r\nimport { nextSort, sortRows, type TableSort } from \"@/registry/miseui/ui/table-state\"\r\n\r\nfunction useControllable<Value>(controlled: Value | undefined, initial: Value, onChange?: (value: Value) => void) {\r\n  const [uncontrolled, setUncontrolled] = useState(initial)\r\n  const value = controlled !== undefined ? controlled : uncontrolled\r\n  const setValue = (next: Value) => {\r\n    setUncontrolled(next)\r\n    onChange?.(next)\r\n  }\r\n  return [value, setValue] as const\r\n}\r\n\r\ninterface DataTableStateOptions<Row> {\r\n  rows: readonly Row[]\r\n  columns: readonly DataTableColumn<Row>[]\r\n  getRowId: (row: Row) => string\r\n  pageSize: number\r\n  defaultSort?: TableSort<string> | null\r\n  locale?: string\r\n  query?: string\r\n  defaultQuery?: string\r\n  onQueryChange?: (query: string) => void\r\n  selected?: readonly string[]\r\n  defaultSelected?: readonly string[]\r\n  onSelectedChange?: (selected: readonly string[]) => void\r\n  hiddenColumns?: readonly string[]\r\n  defaultHiddenColumns?: readonly string[]\r\n  onHiddenColumnsChange?: (hidden: readonly string[]) => void\r\n}\r\n\r\nconst EMPTY: readonly string[] = []\r\n\r\nfunction columnActions<Row>(columns: readonly DataTableColumn<Row>[], hidden: readonly string[], setHidden: (next: readonly string[]) => void) {\r\n  const allKeys = columns.map((column) => column.key)\r\n  return {\r\n    visibleColumns: columns.filter((column) => !hidden.includes(column.key)),\r\n    columns,\r\n    hidden,\r\n    isOnlyVisible: (key: string) => allKeys.filter((entry) => !hidden.includes(entry)).length === 1 && !hidden.includes(key),\r\n    toggleColumn: (key: string) => setHidden(toggleColumn(hidden, key, allKeys)),\r\n  }\r\n}\r\n\r\nfunction selectionActions<Row>(selected: readonly string[], setSelected: (next: readonly string[]) => void, pageIds: readonly string[], selectedRows: readonly Row[]) {\r\n  return {\r\n    selected,\r\n    selectedRows,\r\n    pageChecked: pageChecked(selected, pageIds),\r\n    toggleRow: (id: string) => setSelected(toggleId(selected, id)),\r\n    togglePage: () => setSelected(togglePage(selected, pageIds)),\r\n    clearSelection: () => setSelected(EMPTY),\r\n  }\r\n}\r\n\r\nfunction useDataTable<Row>(options: DataTableStateOptions<Row>) {\r\n  const { rows, columns, getRowId, pageSize, locale } = options\r\n  const [query, setQueryValue] = useControllable(options.query, options.defaultQuery ?? \"\", options.onQueryChange)\r\n  const [selected, setSelected] = useControllable(options.selected, options.defaultSelected ?? EMPTY, options.onSelectedChange)\r\n  const [hidden, setHidden] = useControllable(options.hiddenColumns, options.defaultHiddenColumns ?? EMPTY, options.onHiddenColumnsChange)\r\n  const [sort, setSortValue] = useState<TableSort<string> | null>(options.defaultSort ?? null)\r\n  const [rawPage, setPage] = useState(1)\r\n\r\n  const filtered = useMemo(() => filterRows(rows, columns, query), [rows, columns, query])\r\n  const sorted = useMemo(() => {\r\n    const column = sort ? columns.find((entry) => entry.key === sort.key) : undefined\r\n    if (!sort || !column) return filtered\r\n    return sortRows(filtered, (row) => columnValue(column, row), sort.direction, locale)\r\n  }, [filtered, columns, sort, locale])\r\n\r\n  const pageCount = pageCountOf(sorted.length, pageSize)\r\n  const page = Math.min(rawPage, pageCount)\r\n  const pageRows = pageSlice(sorted, page, pageSize)\r\n  const pageIds = pageRows.map(getRowId)\r\n\r\n  return {\r\n    query,\r\n    // A narrower list may have fewer pages, and page 3 of something new means nothing.\r\n    setQuery: (next: string) => {\r\n      setQueryValue(next)\r\n      setPage(1)\r\n    },\r\n    sort,\r\n    setSort: (next: TableSort<string> | null) => {\r\n      setSortValue(next)\r\n      setPage(1)\r\n    },\r\n    cycleSort: (key: string) => {\r\n      setSortValue((current) => nextSort(current, key))\r\n      setPage(1)\r\n    },\r\n    page,\r\n    setPage,\r\n    pageCount,\r\n    pageSize,\r\n    pageRows,\r\n    range: pageRange(page, pageSize, sorted.length),\r\n    total: sorted.length,\r\n    getRowId,\r\n    ...columnActions(columns, hidden, setHidden),\r\n    ...selectionActions(selected, setSelected, pageIds, rows.filter((row) => selected.includes(getRowId(row)))),\r\n  }\r\n}\r\n\r\ntype DataTableState<Row> = ReturnType<typeof useDataTable<Row>>\r\n\r\nexport { useDataTable, type DataTableState, type DataTableStateOptions }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/data-table-toolbar.tsx",
      "content": "import type * as React from \"react\"\r\nimport { useId } from \"react\"\r\nimport { Columns3, Search } from \"lucide-react\"\r\nimport { Button } from \"@/registry/miseui/ui/button\"\r\nimport { Checkbox } from \"@/registry/miseui/ui/checkbox\"\r\nimport { sortFromOption, sortToOption } from \"@/registry/miseui/ui/data-table-math\"\r\nimport type { DataTableState } from \"@/registry/miseui/ui/data-table-state\"\r\nimport { Input } from \"@/registry/miseui/ui/input\"\r\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/registry/miseui/ui/popover\"\r\nimport { cn } from \"@/registry/miseui/lib/utils\"\r\n\r\ninterface DataTableLabels {\r\n  filter: string\r\n  columns: string\r\n  sort: string\r\n  unsorted: string\r\n  ascending: string\r\n  descending: string\r\n  clearSelection: string\r\n  clearFilter: string\r\n  selectPage: string\r\n  selectRow: (row: string) => string\r\n  selectedCount: (count: number) => string\r\n  range: (from: number, to: number, total: number) => string\r\n  noMatch: (query: string) => string\r\n  loading: string\r\n}\r\n\r\nfunction ColumnMenu<Row>({ table, label, compact }: { table: DataTableState<Row>; label: string; compact: boolean }) {\r\n  const hideable = table.columns.filter((column) => column.hideable !== false)\r\n  if (hideable.length === 0) return null\r\n  return (\r\n    <Popover>\r\n      <PopoverTrigger asChild>\r\n        <Button variant=\"outline\" size=\"icon\" aria-label={label} className={cn(\"shrink-0 pointer-coarse:size-10\", compact && \"size-8\")}>\r\n          <Columns3 aria-hidden=\"true\" />\r\n        </Button>\r\n      </PopoverTrigger>\r\n      <PopoverContent align=\"end\" className=\"w-48 p-1\">\r\n        <ul data-slot=\"data-table-columns\" aria-label={label} className=\"grid grid-cols-[minmax(0,1fr)]\">\r\n          {hideable.map((column) => (\r\n            <ColumnRow key={column.key} header={column.header} checked={!table.hidden.includes(column.key)} locked={table.isOnlyVisible(column.key)} onToggle={() => table.toggleColumn(column.key)} />\r\n          ))}\r\n        </ul>\r\n      </PopoverContent>\r\n    </Popover>\r\n  )\r\n}\r\n\r\nfunction ColumnRow({ header, checked, locked, onToggle }: { header: string; checked: boolean; locked: boolean; onToggle: () => void }) {\r\n  const id = useId()\r\n  return (\r\n    <li>\r\n      <label htmlFor={id} className={cn(\"flex h-8 items-center gap-2 rounded-sm px-2 text-[13px] pointer-coarse:h-10\", locked ? \"opacity-50\" : \"cursor-pointer hover:bg-accent\")}>\r\n        <Checkbox id={id} checked={checked} disabled={locked} onCheckedChange={onToggle} />\r\n        <span className=\"min-w-0 truncate\">{header}</span>\r\n      </label>\r\n    </li>\r\n  )\r\n}\r\n\r\n/** Cards have no column heads to press, so a narrow container needs another way to sort. Native, because this is the phone layout. */\r\nfunction CardSort<Row>({ table, labels }: { table: DataTableState<Row>; labels: DataTableLabels }) {\r\n  const sortable = table.columns.filter((column) => column.sortable !== false)\r\n  return (\r\n    <select\r\n      data-slot=\"data-table-sort\"\r\n      aria-label={labels.sort}\r\n      value={sortToOption(table.sort)}\r\n      onChange={(event) => table.setSort(sortFromOption(event.target.value))}\r\n      className=\"h-9 max-w-28 min-w-0 shrink rounded-md border border-input bg-background px-2 text-[13px] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 pointer-coarse:h-10 @md:hidden\"\r\n    >\r\n      <option value=\"\">{labels.unsorted}</option>\r\n      {sortable.flatMap((column) => [\r\n        <option key={`${column.key}:ascending`} value={`${column.key}:ascending`}>\r\n          {`${column.header}, ${labels.ascending}`}\r\n        </option>,\r\n        <option key={`${column.key}:descending`} value={`${column.key}:descending`}>\r\n          {`${column.header}, ${labels.descending}`}\r\n        </option>,\r\n      ])}\r\n    </select>\r\n  )\r\n}\r\n\r\ninterface ToolbarProps<Row> {\r\n  table: DataTableState<Row>\r\n  labels: DataTableLabels\r\n  compact: boolean\r\n  cards: boolean\r\n  /** Owner's actions on the chosen rows. Filled, the row of filters gives way to them while anything is selected. */\r\n  bulkActions?: (rows: readonly Row[], clear: () => void) => React.ReactNode\r\n  actions?: React.ReactNode\r\n  className?: string\r\n}\r\n\r\nfunction DataTableToolbar<Row>({ table, labels, compact, cards, bulkActions, actions, className }: ToolbarProps<Row>) {\r\n  const count = table.selected.length\r\n  const rowClass = cn(\"flex min-h-9 flex-wrap items-center gap-2 pointer-coarse:min-h-10\", compact && \"min-h-8\", className)\r\n  if (bulkActions && count > 0) {\r\n    return (\r\n      <div data-slot=\"data-table-bulk\" className={rowClass}>\r\n        <p role=\"status\" className=\"text-[13px] font-medium tabular-nums\">\r\n          {labels.selectedCount(count)}\r\n        </p>\r\n        <Button variant=\"ghost\" size=\"sm\" className=\"pointer-coarse:h-10\" onClick={table.clearSelection}>\r\n          {labels.clearSelection}\r\n        </Button>\r\n        <div className=\"ms-auto flex flex-wrap items-center gap-2\">{bulkActions(table.selectedRows, table.clearSelection)}</div>\r\n      </div>\r\n    )\r\n  }\r\n  return (\r\n    <div data-slot=\"data-table-toolbar\" className={rowClass}>\r\n      <div className=\"relative min-w-24 flex-1\">\r\n        <Search aria-hidden=\"true\" className=\"pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground\" />\r\n        <Input\r\n          type=\"search\"\r\n          aria-label={labels.filter}\r\n          placeholder={labels.filter}\r\n          value={table.query}\r\n          onChange={(event) => table.setQuery(event.target.value)}\r\n          className={cn(\"pl-8 pointer-coarse:h-10\", compact && \"h-8 md:text-[13px]\")}\r\n        />\r\n      </div>\r\n      {cards ? <CardSort table={table} labels={labels} /> : null}\r\n      {actions}\r\n      <ColumnMenu table={table} label={labels.columns} compact={compact} />\r\n    </div>\r\n  )\r\n}\r\n\r\nexport { DataTableToolbar, type DataTableLabels }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/data-table-views.tsx",
      "content": "import type * as React from \"react\"\r\nimport { Button } from \"@/registry/miseui/ui/button\"\r\nimport { Checkbox } from \"@/registry/miseui/ui/checkbox\"\r\nimport { columnText, type DataTableColumn } from \"@/registry/miseui/ui/data-table-math\"\r\nimport type { DataTableState } from \"@/registry/miseui/ui/data-table-state\"\r\nimport type { DataTableLabels } from \"@/registry/miseui/ui/data-table-toolbar\"\r\nimport { Skeleton } from \"@/registry/miseui/ui/skeleton\"\r\nimport { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow, TableSortHead, type TableVariant } from \"@/registry/miseui/ui/table\"\r\nimport { cn } from \"@/registry/miseui/lib/utils\"\r\n\r\nconst cellOf = <Row,>(column: DataTableColumn<Row>, row: Row): React.ReactNode => (column.cell ? column.cell(row) : columnText(column, row))\r\n\r\n/** The label is the hit target: a 16 px box stays 16 px while the press area reaches 32, or 40 on touch. */\r\nfunction SelectBox({ label, checked, onToggle }: { label: string; checked: boolean | \"indeterminate\"; onToggle: () => void }) {\r\n  return (\r\n    <label className=\"-mx-2 inline-flex size-8 cursor-pointer items-center justify-center align-middle pointer-coarse:size-10\">\r\n      <Checkbox aria-label={label} checked={checked} onCheckedChange={onToggle} />\r\n    </label>\r\n  )\r\n}\r\n\r\ninterface ViewProps<Row> {\r\n  table: DataTableState<Row>\r\n  labels: DataTableLabels\r\n  selectable: boolean\r\n  loading: boolean\r\n  loadingRows: number\r\n  emptyText: string\r\n}\r\n\r\nfunction EmptyMessage<Row>({ table, labels, emptyText }: Pick<ViewProps<Row>, \"table\" | \"labels\" | \"emptyText\">) {\r\n  if (!table.query) return <p>{emptyText}</p>\r\n  return (\r\n    <div className=\"grid justify-items-center gap-2\">\r\n      <p className=\"break-words\">{labels.noMatch(table.query)}</p>\r\n      <Button variant=\"outline\" size=\"sm\" className=\"pointer-coarse:h-10\" onClick={() => table.setQuery(\"\")}>\r\n        {labels.clearFilter}\r\n      </Button>\r\n    </div>\r\n  )\r\n}\r\n\r\nfunction HeadCell<Row>({ table, column }: { table: DataTableState<Row>; column: DataTableColumn<Row> }) {\r\n  if (column.sortable === false) {\r\n    return (\r\n      <TableHead numeric={column.numeric} className={column.className}>\r\n        {column.header}\r\n      </TableHead>\r\n    )\r\n  }\r\n  return (\r\n    <TableSortHead numeric={column.numeric} className={column.className} direction={table.sort?.key === column.key ? table.sort.direction : \"none\"} onSort={() => table.cycleSort(column.key)}>\r\n      {column.header}\r\n    </TableSortHead>\r\n  )\r\n}\r\n\r\nfunction SkeletonRows<Row>({ table, selectable, loadingRows }: Pick<ViewProps<Row>, \"table\" | \"selectable\" | \"loadingRows\">) {\r\n  return Array.from({ length: loadingRows }, (_, index) => (\r\n    <TableRow key={index} data-slot=\"data-table-skeleton\" aria-hidden=\"true\">\r\n      {selectable ? (\r\n        <TableCell className=\"w-10\">\r\n          <Skeleton className=\"size-4\" />\r\n        </TableCell>\r\n      ) : null}\r\n      {table.visibleColumns.map((column) => (\r\n        <TableCell key={column.key} numeric={column.numeric}>\r\n          <Skeleton className={cn(\"h-4 max-w-24\", column.numeric && \"ms-auto w-12\")} />\r\n        </TableCell>\r\n      ))}\r\n    </TableRow>\r\n  ))\r\n}\r\n\r\ninterface TableViewProps<Row> extends ViewProps<Row> {\r\n  variant: TableVariant\r\n  effect?: \"spring\"\r\n  label: string\r\n  className?: string\r\n}\r\n\r\nfunction DataRow<Row>({ row, table, labels, selectable }: Pick<ViewProps<Row>, \"table\" | \"labels\" | \"selectable\"> & { row: Row }) {\r\n  const id = table.getRowId(row)\r\n  const isSelected = table.selected.includes(id)\r\n  const primary = table.visibleColumns[0]\r\n  return (\r\n    <TableRow selected={isSelected}>\r\n      {selectable ? (\r\n        <TableCell className=\"w-10\">\r\n          <SelectBox label={labels.selectRow(primary ? columnText(primary, row) : id)} checked={isSelected} onToggle={() => table.toggleRow(id)} />\r\n        </TableCell>\r\n      ) : null}\r\n      {table.visibleColumns.map((column) => (\r\n        <TableCell key={column.key} numeric={column.numeric} className={column.className}>\r\n          {cellOf(column, row)}\r\n        </TableCell>\r\n      ))}\r\n    </TableRow>\r\n  )\r\n}\r\n\r\nfunction DataTableTable<Row>({ table, labels, selectable, loading, loadingRows, emptyText, variant, effect, label, className }: TableViewProps<Row>) {\r\n  const colSpan = table.visibleColumns.length + (selectable ? 1 : 0)\r\n  return (\r\n    <Table variant={variant} effect={effect} label={label} className={className}>\r\n      <TableHeader>\r\n        <TableRow>\r\n          {selectable ? (\r\n            <TableHead className=\"w-10\">\r\n              <SelectBox label={labels.selectPage} checked={table.pageChecked} onToggle={table.togglePage} />\r\n            </TableHead>\r\n          ) : null}\r\n          {table.visibleColumns.map((column) => (\r\n            <HeadCell key={column.key} table={table} column={column} />\r\n          ))}\r\n        </TableRow>\r\n      </TableHeader>\r\n      <TableBody>\r\n        {loading ? <SkeletonRows table={table} selectable={selectable} loadingRows={loadingRows} /> : null}\r\n        {!loading && table.pageRows.length === 0 ? (\r\n          <TableEmpty colSpan={colSpan}>\r\n            <EmptyMessage table={table} labels={labels} emptyText={emptyText} />\r\n          </TableEmpty>\r\n        ) : null}\r\n        {!loading ? table.pageRows.map((row) => <DataRow key={table.getRowId(row)} row={row} table={table} labels={labels} selectable={selectable} />) : null}\r\n      </TableBody>\r\n    </Table>\r\n  )\r\n}\r\n\r\nfunction DataTableCard<Row>({ row, table, labels, selectable }: Pick<ViewProps<Row>, \"table\" | \"labels\" | \"selectable\"> & { row: Row }) {\r\n  const id = table.getRowId(row)\r\n  const isSelected = table.selected.includes(id)\r\n  const [primary, ...rest] = table.visibleColumns\r\n  return (\r\n    <li data-slot=\"data-table-card\" data-selected={isSelected ? \"true\" : undefined} className=\"grid grid-cols-[minmax(0,1fr)] gap-2 rounded-lg border bg-background p-3 data-[selected=true]:border-transparent data-[selected=true]:bg-mise-soft\">\r\n      <div className=\"flex min-w-0 items-center gap-2\">\r\n        {selectable ? <SelectBox label={labels.selectRow(primary ? columnText(primary, row) : id)} checked={isSelected} onToggle={() => table.toggleRow(id)} /> : null}\r\n        <p className=\"min-w-0 text-sm font-medium break-words\">{primary ? cellOf(primary, row) : id}</p>\r\n      </div>\r\n      {rest.length > 0 ? (\r\n        <dl className=\"grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-[13px]\">\r\n          {rest.map((column) => (\r\n            <div key={column.key} className=\"col-span-2 grid grid-cols-subgrid\">\r\n              <dt className=\"text-muted-foreground\">{column.header}</dt>\r\n              <dd className={cn(\"min-w-0 text-right break-words\", column.numeric && \"tabular-nums\")}>{cellOf(column, row)}</dd>\r\n            </div>\r\n          ))}\r\n        </dl>\r\n      ) : null}\r\n    </li>\r\n  )\r\n}\r\n\r\nfunction DataTableCards<Row>({ table, labels, selectable, loading, loadingRows, emptyText, className }: ViewProps<Row> & { className?: string }) {\r\n  if (loading) {\r\n    return (\r\n      <div aria-hidden=\"true\" className={cn(\"grid grid-cols-[minmax(0,1fr)] gap-2\", className)}>\r\n        {Array.from({ length: loadingRows }, (_, index) => (\r\n          <Skeleton key={index} data-testid=\"data-table-card-skeleton\" className=\"h-24 rounded-lg\" />\r\n        ))}\r\n      </div>\r\n    )\r\n  }\r\n  if (table.pageRows.length === 0) {\r\n    return (\r\n      <div className={cn(\"grid min-h-32 place-items-center rounded-lg border p-4 text-center text-sm text-muted-foreground\", className)}>\r\n        <EmptyMessage table={table} labels={labels} emptyText={emptyText} />\r\n      </div>\r\n    )\r\n  }\r\n  return (\r\n    <div className={cn(\"grid grid-cols-[minmax(0,1fr)] gap-2\", className)}>\r\n      {selectable ? (\r\n        <div className=\"flex items-center gap-2 px-3 text-[13px] text-muted-foreground\">\r\n          <SelectBox label={labels.selectPage} checked={table.pageChecked} onToggle={table.togglePage} />\r\n          <span aria-hidden=\"true\">{labels.selectPage}</span>\r\n        </div>\r\n      ) : null}\r\n      <ul data-slot=\"data-table-cards\" className=\"grid grid-cols-[minmax(0,1fr)] gap-2\">\r\n        {table.pageRows.map((row) => (\r\n          <DataTableCard key={table.getRowId(row)} row={row} table={table} labels={labels} selectable={selectable} />\r\n        ))}\r\n      </ul>\r\n    </div>\r\n  )\r\n}\r\n\r\nexport { DataTableCards, DataTableTable }\r\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}