Skip to content
DocsSupportPricing
Roadmap (has updates coming soon)XFigma3.3K
Overview
  • Introduction
  • Get Started
  • License Setup
  • Styling
  • Registry
  • MCP Server
  • Agent Skills
  • RTL
  • Changelog
MCP Server
  • Claude
  • CodexCodexCodex
  • Cursor
  • Grok
  • Conductor
  • v0
  • Lovable
  • Replit
  • Bolt
  • OpenCode
  • VS Code
  • GitHub Copilot
  • Kilo Code
  • Zed
  • Antigravity
Components
  • Alert
  • Autocomplete
  • Badge
  • Data GridRebuilt on TanStack Table v9, with pinning now start/end
  • Date Selector
  • Event CalendarNew event calendar component with five views
  • File Upload
  • FiltersSizing and interaction refinements
  • Frame
  • GanttNew gantt component with day to year scales
  • Icon Stack
  • Icon TileNew icon tile component with five surface variants
  • KanbanonValueCommit callback and accessibility improvements
  • Number Field
  • Phone Input
  • Rating
  • Scrollspy
  • SortableonValueCommit callback and accessibility improvements
  • StepperRender prop composition and styling refinements
  • Timeline
  • Tree

Application

  • App Shell
  • Auth
  • Card
  • Chart
  • Dashboard
  • Dialog
  • Empty State
  • Event CalendarThree new event calendar blocks added
  • Form
  • GanttNew gantt block added
  • Kanban Board
  • List
  • Navbar
  • Onboarding
  • Profile
  • Schedule
  • Settings
  • Sheet
  • Stats
  • Timeline
  • Wizard

Solutions

  • Agents
  • AI Ops
  • Analytics
  • Billing
  • Bookings
  • CRM
  • Files
  • Inventory
  • Users

Templates

  • E-commerce
  • SaaS
  • Dashboard
  • Landing
  • All templates

eCommerce

  • Category Card
  • Checkout
  • Comparison
  • Coupon
  • Filter Sidebar
  • Product Card
  • Product Detail
  • Product Grid
  • Receipt
  • Review
  • Shopping Cart
  • Wishlist

Data Grid

  • Base
  • Columns
  • Drag & DropNew drag and drop Data Grid block added
  • EditingNew editable Data Grid block added
  • Expansion
  • Filtering
  • GroupingFour new grouped and tree Data Grid blocks added
  • Virtualization

Marketing

  • Blog
  • Contact
  • CTA
  • FAQ

Resources

  • Components
  • Blocks
  • Docs
  • Support
  • Pricing
  • Roadmap(has updates coming soon)
  • AffiliateSoon

Legal

  • Privacy Policy
  • Terms & Conditions
  • License
  • Refunds
  • Cookies

© 2026 ReUI. All rights reserved.

3.3K

Shadcn Data Grid

PreviousNext

Custom Shadcn Data Grid for React and Tailwind CSS. A powerful TanStack Table v9 data grid with sorting, filtering, pagination, footer rows, drag-and-drop, virtualization, infinite scroll, row pinning, and tree rows.

Base UIRadix UI
API Reference

The current data-grid package ships the shared grid context, table renderers, pagination, column controls, drag-and-drop helpers, virtualization, infinite scroll, footer helpers, row-pinning support, and tree rows. Footer components (DataGridTableFoot, DataGridTableFootRow, DataGridTableFootRowCell) plus the DataGridTableRowPin and DataGridTableRowExpand toggles are exported from data-grid-table.tsx.

In the base build, data-grid-scroll-area.tsx is also included and exports DataGridScrollArea for dedicated scroll handling around sticky-header or wide tables.

Installation

pnpm dlx shadcn@latest add @reui/data-grid

Shadcn Data Grid Free Components

Browse 30 production-ready Shadcn Data Grid components for dashboards, forms, and product UI. These examples use Base UI primitives from @base-ui/react and stay fully compatible with Shadcn Create so radius, color, and typography match your configured theme.

Browse all 30 Shadcn Data Grid components for copy-ready layouts, dashboards, and forms built with Tailwind CSS in the ReUI library.

Shadcn Data Grid Pro Blocks

This primitive also powers ready-made ReUI Pro blocks: complete data grid sections assembled on top of the exact component documented here, with realistic data, polished layout details, and production interaction patterns you can copy straight into your app.

Preview all 35 Shadcn Data Grid Pro blocks in the ReUI blocks gallery.

BadgeDate Selector

On This Page

InstallationTanStack Table v9Prompt for TanStack Table v9 migrationReact CompilerUsageExamplesCell BorderDense TableLight TableStriped TableAuto WidthRow SelectionTree RowsAPI ReferenceDataGridDataGridTableClassNamesDataGridContainerDataGridScrollAreaDataGridTableDataGridPaginationDataGridTableDndDataGridTableDndRowsDataGridTableVirtualDataGridTableRowExpandDataGridTableFootDataGridTableFootRowDataGridTableFootRowCellDOM Attributes

TanStack Table v9

The data grid is built on TanStack Table v9. v9 asks every table to declare which features it uses, so the grid exports a ready-made bundle:

import { useTable } from "@tanstack/react-table"
 
import { dataGridFeatures } from "@/components/reui/data-grid"
 
const table = useTable({
  features: dataGridFeatures,
  columns,
  data,
  state: { sorting, pagination },
  onSortingChange: setSorting,
  onPaginationChange: setPagination,
})

dataGridFeatures registers everything the grid's own rendering needs, which is a wider set than it looks. columnVisibilityFeature gates row.getVisibleCells() and columnPinningFeature gates the getStartVisibleCells() / getCenterVisibleCells() / getEndVisibleCells() split every row goes through, so a grid that never hides and never pins a column still needs both just to render rows.

Two entries in the bundle are neither features nor row models:

  • sortFns, which registers every built-in v9 ships: alphanumeric, alphanumericCaseSensitive, basic, datetime, text and textCaseSensitive. On v9 a string sortFn resolves against this registry alone, and the default sortFn: "auto" infers one of those names from the first row's value, so the map has to be complete or an ordinary string column warns in development and falls back to unsorted order. Register a custom comparator by extending the bundle with tableFeatures({ ...dataGridFeatures, sortFns: { ...} }), or pass a function directly as sortFn.
  • columnMeta, which types columnDef.meta as DataGridColumnMeta (headerTitle, headerClassName, cellClassName, skeleton, expandedContent, autoSize). A features-level columnMeta slot wins over the global ColumnMeta interface, so the v8 declare module augmentation is ignored on any table built with dataGridFeatures. Add your own fields to DataGridColumnMeta in the installed data-grid.tsx instead.

The bundle also ships as a type, DataGridFeatures. v9 puts the feature set first in the TanStack generics, so that is the type you write wherever a column, a row or the table appears in your own annotations:

import type { ColumnDef, Row } from "@tanstack/react-table"
 
import type { DataGridFeatures } from "@/components/reui/data-grid"
 
const columns: ColumnDef<DataGridFeatures, IData>[] = [
  // ...
]
 
function ActionsCell({ row }: { row: Row<DataGridFeatures, IData> }) {
  // ...
}

If you build your own bundle instead, annotate against that one (typeof features) rather than DataGridFeatures.

You still own the table. <DataGrid> accepts any feature bundle, so extend it when a grid needs more:

const features = tableFeatures({
  ...dataGridFeatures,
  columnGroupingFeature,
  groupedRowModel: createGroupedRowModel(),
})

or hand over a leaner one of your own when you want a smaller bundle. <DataGrid> is the generic one: its table prop is Table<TFeatures, TData> for any TFeatures, and the instance is widened internally exactly once. The sub-components that take a column, row or table prop (DataGridColumnHeader, DataGridColumnFilter, DataGridColumnVisibility, DataGridTableRowSelect, DataGridTableRowPin, DataGridTableRowExpand) are declared against DataGridFeatures, and TFeatures is invariant in v9, so any bundle that is wider or leaner than dataGridFeatures needs a cast at those boundaries.

Prompt for TanStack Table v9 migration

If you already have the Data Grid installed, paste the prompt below into your coding agent. It covers both cases: a session with the ReUI MCP connected, and a plain project where the primitive is reinstalled from the registry.

Migrate ReUI Data Grid to TanStack Table v9.
 
STEP 1 - Replace the Data Grid primitive from the registry.
 
  Case A, the ReUI MCP is available in this session:
    - Call search("data-grid"), then get_component("data-grid") to read the current API.
    - Call get_examples("data-grid") to see real composition.
    - Run the command returned by get_install_command("data-grid") and overwrite when prompted.
 
  Case B, no ReUI MCP:
    - Run: npx shadcn@latest add @reui/data-grid
    - Answer yes when asked to overwrite the existing files.
 
  Every data-grid file must come from the registry. Do not hand-patch them, do not merge
  them by hand, and do not keep a local fork of any of them.
 
STEP 2 - Move to TanStack Table v9 and confirm v8 is gone.
 
  - Set "@tanstack/react-table" to "^9.0.0" in package.json.
  - Reinstall (npm install, pnpm install or yarn).
  - Do not continue until all three checks pass:
      1. package.json lists "@tanstack/react-table": "^9.0.0" and no other version of it.
      2. The installed version is 9.x. Run: npm ls @tanstack/react-table
      3. No v8 copy survives in the lockfile. Search the lockfile for "react-table" and
         confirm every resolved version is 9.x. If an 8.x entry remains, another dependency
         is pulling it in: dedupe or upgrade that package.
  - This matters because v9 removed useReactTable entirely. A leftover v8 install under v9
    code fails as an unresolved import, not as a helpful error.
 
STEP 3 - Update every table you own.
 
  - useReactTable({ ... }) becomes useTable({ features: dataGridFeatures, ... }), importing
    dataGridFeatures from your installed data-grid.
  - Delete the getCoreRowModel / getSortedRowModel / getFilteredRowModel /
    getPaginationRowModel options. dataGridFeatures already registers them.
  - If a grid used to render every row, add manualPagination: true. The v9 paginated row
    model always slices, so that grid would otherwise show only the first page.
  - Add the leading TFeatures generic to types: ColumnDef<TData, TValue> becomes
    ColumnDef<DataGridFeatures, TData, TValue>. Same for Row, Column, Cell, Header, Table.
  - Column pinning is start/end, never left/right: ColumnPinningState, column.pin(),
    getIsPinned(), and any CSS matching [data-pinned="left"].
  - Renames: table.getState() becomes table.state, VisibilityState becomes
    ColumnVisibilityState, sortingFn becomes sortFn, columnSizingInfo becomes columnResizing.
  - Indeterminate header checkbox: use
    getIsSomePageRowsSelected() && !getIsAllPageRowsSelected().
  - Column meta comes from the exported DataGridColumnMeta. Delete any
    declare module "@tanstack/react-table" augmentation you added.
  - Remove "use no memo" from files that only wrap the grid.
  - State shapes got stricter: ColumnPinningState requires both start and end,
    RowPinningState requires both top and bottom, and RowSelectionState narrowed to
    Record<string, true>, so deselecting by writing false no longer type-checks.
 
STEP 4 - Prove the migration is complete.
 
  Search the whole project. Every one of these must return zero results:
    useReactTable
    getCoreRowModel
    data-pinned="left"
    table.getState(
    declare module "@tanstack/react-table"
  Any hit is a call site you missed.
 
STEP 5 - Verify it actually runs.
 
  Typecheck must pass with no errors. Then open each grid and confirm: sorting, paging,
  column visibility, pinning a column to each side while scrolling horizontally, row
  selection including the header checkbox, and column resizing.

React Compiler

TanStack Table v8 returned a stable table instance whose state mutated internally, so React Compiler memoized reads against a reference that never changed and state updates could be skipped. That is what the "use no memo" directive worked around, and on v9 it is no longer needed here: useTable returns a fresh table reference on every state change, which is exactly the signal the compiler needs.

One gap remains, and the grid already handles it for you. State is not only read through table, it is also read through builder calls like row.getIsSelected() and column.getIsPinned(). Those hide their dependency from the compiler, and when a header or cell is rendered by a nested component holding a stable row or column, the compiler can memoize that JSX and never re-run the read. ReUI wraps its own such reads - the selection checkboxes and the column header - in TanStack's Subscribe, so sort arrows, pin controls and checkboxes stay live.

What you must do. If your own cell or header template is a named child component that reads state through a builder call, subscribe there too:

import { Subscribe } from "@tanstack/react-table"
 
cell: ({ row }) => (
  <Subscribe source={row.table.atoms.rowSelection}>
    {() => <MyCheckbox checked={row.getIsSelected()} />}
  </Subscribe>
)

Note the standalone Subscribe rather than table.Subscribe: inside a column definition the table you receive is typed as the core Table from @tanstack/table-core, and only the ReactTable that useTable returns declares Subscribe, so row.table.Subscribe does not type-check.

Usage

import { useTable, type ColumnDef } from "@tanstack/react-table"
 
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  DataGridPagination,
  DataGridTable,
  DataGridTableFootRow,
  DataGridTableFootRowCell,
  type DataGridFeatures,
} from "@/components/reui/data-grid"
const columns: ColumnDef<DataGridFeatures, User>[] = [
  { accessorKey: "name", header: "Name" },
  { accessorKey: "email", header: "Email" },
]
 
const footer = (
  <DataGridTableFootRow>
    <DataGridTableFootRowCell colSpan={columns.length}>
      Showing {data.length} rows
    </DataGridTableFootRowCell>
  </DataGridTableFootRow>
)
 
const table = useTable({
  features: dataGridFeatures,
  data,
  columns,
})
 
return (
  <DataGrid
    table={table}
    recordCount={data.length}
    tableLayout={{ rowsPinnable: true }}
  >
    <DataGridContainer>
      <DataGridTable footerContent={footer} />
    </DataGridContainer>
    <DataGridPagination />
  </DataGrid>
)

Use DataGridTableRowPin inside a column definition to let users pin rows, swap in DataGridTableVirtual when you need virtualization or infinite scroll, and wrap sticky-header tables with DataGridScrollArea when you want the dedicated base scroll wrapper. When rows can be pinned or virtualized, provide a stable getRowId so row identity stays intact across reordering.

Examples

Cell Border

Dense Table

Light Table

Striped Table

Auto Width

Row Selection

Tree Rows

For the full set of variations (expandable rows, sub-grids, tree rows, sortable / movable / draggable / resizable / pinnable columns, sticky header, column controls and visibility, loading skeleton, CRUD in frame, footer totals / summary / aggregates, infinite scroll, and row pinning) browse the Data Grid components.

API Reference

DataGrid

The root component that provides the table context.

PropTypeDefaultDescription
tableTable<TFeatures, TData>-Required. The TanStack Table instance.
recordCountnumber-Required. Total number of records.
isLoadingbooleanfalseWhether the table is in a loading state.
loadingMode"skeleton" | "spinner""skeleton"The visual style of the loading state.
loadingMessageReactNode | string"Loading..."Message to display when loadingMode is "spinner".
fetchingMoreMessageReactNode | stringloadingMessageMessage to display while DataGridTableVirtual is fetching more rows.
allRowsLoadedMessageReactNode | string"All records loaded"Message to display when virtual infinite scroll reaches the end.
emptyMessageReactNode | string"No data available"Message to display when the table is empty.
onRowClick(row: TData) => void-Callback function triggered when a row is clicked.
tableLayoutDataGridTableLayout-Configuration for table layout and features.
tableClassNamesDataGridTableClassNames-Custom CSS classes for various table parts.
classNamestring-Additional CSS classes for the root grid component.

PropertyTypeDefaultDescription
densebooleanfalseWhether to use dense padding for cells.
cellBorderbooleanfalseWhether to show vertical borders between cells.
rowBorderbooleantrueWhether to show horizontal borders between rows.
rowRoundedbooleanfalseWhether to add rounded corners to rows.
strippedbooleanfalseWhether to use zebra-striping for rows.
headerBackgroundbooleanfalseWhether to show a background color for the header.
footerBackgroundbooleanfalseWhether to show a background color for footer rows.
headerBorderbooleantrueWhether to show a border below the header.
headerStickybooleanfalseWhether the header should be sticky during scroll.
width"auto" | "fixed""fixed"The table layout algorithm (table-auto vs table-fixed).
columnsVisibilitybooleanfalseEnables column visibility toggling.
columnsResizablebooleanfalseEnables column resizing.
columnsResizeMode"onChange" | "onEnd""onEnd"When a column resize is committed.
columnsPinnablebooleanfalseEnables column pinning.
columnsMovablebooleanfalseEnables moving columns via menu.
columnsDraggablebooleanfalseEnables drag-and-drop for columns.
rowsDraggablebooleanfalseEnables drag-and-drop for rows.
rowsPinnablebooleanfalseEnables row pinning (top/bottom).

columnsResizeMode is resolved by the grid, not by TanStack. Left unset, the grid reads the table's own columnResizeMode and falls back to "onEnd" when that is undefined, which on v9 is the normal case: useTable hands back the options object you passed, so feature defaults never show up on table.options.


DataGridTableClassNames

Custom CSS classes for different parts of the table.

PropertyTypeDefaultDescription
basestring-CSS classes for the <table> element.
headerstring-CSS classes for the <thead> element.
headerRowstring-CSS classes for header rows.
headerStickystring-CSS classes for sticky header state.
bodystring-CSS classes for the <tbody> element.
bodyRowstring-CSS classes for body rows.
footerstring-CSS classes for the <tfoot> element.
edgeCellstring-CSS classes for the first and last cells in a row.

DataGridContainer

The outer wrapper for the grid. It clips overflow, so scrolling comes from DataGridScrollArea.

PropTypeDefaultDescription
childrenReactNode-Required. The grid content to wrap.
borderboolean-Accepted for backwards compatibility and currently has no effect.
classNamestring-Additional CSS classes for the container.

DataGridScrollArea

Dedicated scroll wrapper for wide grids and sticky headers.

PropTypeDefaultDescription
childrenReactNode-Required. The grid content to wrap.
orientation"horizontal" | "vertical" | "both""both"Which scrollbars to render.
classNamestring-Additional CSS classes for the wrapper.

While the sticky-header scroll mode is active (headerSticky with a vertical orientation), the root carries data-overflow-vertical="true" whenever content overflows vertically. Use it as an ancestor selector to style scrollable vs short grids, for example a closing bottom border on the last row only when a fixed-height grid is partially filled: [[data-slot=data-grid-scroll-area]:not([data-overflow-vertical])_&:last-child>td]:border-b on tableClassNames.bodyRow.


DataGridTable

The component that renders the actual HTML table. It automatically handles data rendering, loading states (skeletons/spinners), empty states, footer rows, and pinned rows when rowsPinnable is enabled on the parent DataGrid. The unpinned rows come from table.getRowModel(), which on v9 resolves to the paginated row model, and dataGridFeatures always registers paginatedRowModel, so a grid that must render every row sets manualPagination: true on useTable.

PropTypeDefaultDescription
footerContentReactNode-Optional footer content rendered inside <tfoot>.
renderHeaderbooleantrueWhether to render the table header.

DataGridPagination

The component for table pagination controls. The record info comes from the recordCount prop on DataGrid, and the page buttons come from table.getPageCount(), so they only render when there is more than one page. In v9 getPageCount() is pageCount ?? Math.ceil(rowCount / pageSize) and rowCount falls back to the pre-paginated row count, so a grid whose data holds only the current page must pass rowCount or pageCount to useTable or the buttons never appear.

PropTypeDefaultDescription
sizesnumber[][5, 10, 25, 50, 100]Array of available page sizes.
sizesSkeletonReactNode<Skeleton className="h-8 w-44" />Placeholder shown instead of the page size selector while isLoading is set.
rowsPerPageLabelstring"Rows per page"Visible label rendered next to the page size selector.
infostring"{from} - {to} of {count}"Template for the record info. {count} is recordCount.
infoSkeletonReactNode<Skeleton className="h-8 w-60" />Placeholder shown instead of the record info while isLoading is set.
moreLimitnumber5The number of page buttons to show before truncating.
previousPageLabelstring"Go to previous page"Accessible label for the previous page button.
nextPageLabelstring"Go to next page"Accessible label for the next page button.
ellipsisTextstring"..."Text to display for the ellipsis button.
classNamestring-Additional CSS classes for the pagination container.

sizesInfo, sizesLabel, sizesDescription and more are still accepted by DataGridPaginationProps but are not rendered.


PropTypeDefaultDescription
columnColumn<DataGridFeatures, TData, TValue>-Required. The TanStack Column instance.
titlestring-Header label. Falls back to columnDef.meta.headerTitle, then a string columnDef.header, then column.id.
iconReactNode-Optional icon to display next to the title.
filterReactNode-Optional filter component to display in the header menu.
visibilitybooleanfalseWhether to include column visibility controls in the menu.
classNamestring-Additional CSS classes for the header label or trigger button.

PropTypeDefaultDescription
columnColumn<DataGridFeatures, TData, TValue>-The TanStack Column instance to filter.
titlestring-The title for the filter trigger and placeholder.
optionsArray<{ label: string, value: string, icon?: ComponentType<{ className?: string }> }>-Required. The list of options to filter by.

The count beside each option comes from column.getFacetedUniqueValues(). On v9 that method only exists when the feature bundle registers columnFacetingFeature: leave the feature out of a leaner bundle of your own and the call throws, because the method is never added to the column. The numbers themselves come from the facetedUniqueValues row model, and without it the method still answers, with an empty map, so the counts quietly vanish. facetedRowModel is what makes those counts respect the table's other active filters instead of the unfiltered rows. dataGridFeatures registers all three, so the counts work out of the box.

Filtering needs one thing more. v9 resolves a string filterFn name against the filterFns map on the feature bundle, including the default 'auto', and dataGridFeatures registers none, so hand the column a filter function directly:

import { filterFn_arrHas } from "@tanstack/react-table"
 
const columns = [
  {
    accessorKey: "status",
    header: "Status",
    filterFn: filterFn_arrHas,
  },
]

This filter writes an array of selected values, and filterFn_arrHas matches a scalar cell value against that array (reach for filterFn_arrIncludesSome when the cell itself holds an array). Without a function the value still lands in columnFilters, no row is ever filtered, and v9 warns in development that the filter function is not registered. The alternative is to register the names you want in your own bundle through the filterFns slot, the way dataGridFeatures registers sortFns.


PropTypeDefaultDescription
tableTable<DataGridFeatures, TData>-Required. The TanStack Table instance.
triggerReactElement<Record<string, unknown>>-Required. The trigger element for the visibility menu.

DataGridTableDnd

Used for enabling column drag-and-drop reordering with optional footer rendering.

PropTypeDefaultDescription
handleDragEnd(event: DragEndEvent) => void-Required. Callback triggered when a column drag operation ends.
footerContentReactNode-Optional footer content rendered inside <tfoot>.

The sortable items come from table.state.columnOrder, and TanStack starts that slice as an empty array, so you have to seed it and keep it controlled. Give every column an explicit id: ColumnDef.id is optional and is only derived from accessorKey on the built column, not on the definition object you map over.

const [columnOrder, setColumnOrder] = useState<string[]>(() =>
  columns.map((column) => column.id as string)
)
 
const handleDragEnd = ({ active, over }: DragEndEvent) => {
  if (!over || active.id === over.id) return
  setColumnOrder((order) =>
    arrayMove(
      order,
      order.indexOf(active.id as string),
      order.indexOf(over.id as string)
    )
  )
}
 
const table = useTable({
  features: dataGridFeatures,
  columns,
  data,
  state: { columnOrder },
  onColumnOrderChange: setColumnOrder,
})

Leave columnOrder empty and the headers never resolve a position inside the sortable context, so the columns do not shift during the gesture and arrayMove runs against an empty array, which commits nothing. The slice is read as table.state.columnOrder on v9, where v8 used table.getState().columnOrder; setColumnOrder, onColumnOrderChange and ColumnOrderState keep their v8 names and shape.


DataGridTableDndRows

Used for enabling row drag-and-drop reordering with optional footer rendering.

Reordering is yours to commit: the grid carries a clone of the row you picked up and marks the seam it would land on, then hands you handleDragEnd to write the new order back. Three things have to line up, and a reorder that silently does nothing is almost always one of them:

  • getRowId must be stable and match dataIds. dnd-kit identifies rows by row.id, so dataIds has to be the same ids in the same order as the rendered rows. Deriving both from the record's own id is the reliable pattern.
  • Reorder by replacing data, not by mutating it. TanStack reprocesses rows when the data reference changes, so an in-place splice leaves the grid showing the old order.
  • Do not read a stale index. Resolve positions from the current data inside the state updater.
const [data, setData] = useState(rows)
const dataIds = useMemo(() => data.map(({ id }) => id), [data])
 
const handleDragEnd = ({ active, over }: DragEndEvent) => {
  if (!over || active.id === over.id) return
  setData((current) => {
    const from = current.findIndex((row) => row.id === active.id)
    const to = current.findIndex((row) => row.id === over.id)
    return from === -1 || to === -1 ? current : arrayMove(current, from, to)
  })
}
 
const table = useTable({
  features: dataGridFeatures,
  // dataGridFeatures registers a paginated row model, and that model always
  // slices to pageSize (10 by default). manualPagination says the data is
  // already the page, so every row renders and stays reorderable.
  manualPagination: true,
  columns,
  data,
  getRowId: (row) => row.id,
})

The drag handle comes from DataGridTableDndRowHandle in a column of your own. It reads the row's sortable context, so it only works inside the rows this component renders; placed anywhere else it renders as a disabled grip. It also takes className, plus a disabled flag and a disabledLabel (default "Reordering unavailable") for the cases where reordering is genuinely off, a sort being the usual one: the grip keeps its place in the gutter and reads as unavailable instead of vanishing and collapsing the column.

PropTypeDefaultDescription
dataIdsUniqueIdentifier[]-Required. Array of unique identifiers for the current page data.
handleDragEnd(event: DragEndEvent) => void-Required. Callback triggered when a row drag operation ends.
footerContentReactNode-Optional footer content rendered inside <tfoot>.
collisionDetectionCollisionDetectionclosestCenterOverrides the dnd-kit collision strategy.
modifiersModifier[][restrictToVerticalAxis]Replaces the default axis restriction. The vertical clamp to the table container is always applied after these. The horizontal one is dropped so a tree drag can read x.
sortingStrategySortingStrategyhold in placeReplaces the strategy that holds every row where it is. Pass verticalListSortingStrategy for the classic sliding gap.
renderRowDecoration(context) => ReactNode-Per-row slot for drop indicators and depth guides. Receives { row, isDragging, isOver }.
dropIndicatorbooleantrueMarks the drop target with a bar down its leading edge. Turn off when renderRowDecoration paints its own.
onDragStart(event: DragStartEvent) => void-Forwarded from the drag context, after the internal drag state updates.
onDragMove(event: DragMoveEvent) => void-Forwarded from the drag context.
onDragOver(event: DragOverEvent) => void-Forwarded from the drag context.
onDragCancel(event: DragCancelEvent) => void-Forwarded from the drag context, after the internal drag state resets.

While a row is in flight the grid does four things, and all of them are built in:

  • The carried row is a clone, measured from the row it was lifted from - its column widths and its height. It is not a fixed size, so a grid with wrapping cells or dense rows does not appear to grow under the pointer when you pick a row up.
  • The rows hold still. Sliding them apart to open a gap reads well in a list of identical rows and badly in a table: the gap is the height of the row you are holding, so with rows of unequal height it never matches the slot it claims to be, and the row you picked up slides away from where it started. Pass verticalListSortingStrategy as sortingStrategy for the old behaviour.
  • The row you picked up stays where it was, dimmed and outlined. It is the slot you are moving out of, so it is still there to return to if you change your mind mid-drag.
  • The drop target is marked with a 2px bar down its leading edge, the same marker the tree drag uses. Since nothing moves, the bar is the only thing that says where the row lands, and data-edge says which side of that row it comes to rest on.

The indicator renders as a plain element inside the last cell, never a td of its own, so it adds no column and cannot disturb table-layout: fixed. Target it with [data-slot="data-grid-table-row-drop-indicator"]; it carries data-edge="top" | "bottom" for the side the row would land on, so you can style your own seam from it.

Every sortable row carries tree metadata, so any drag event can resolve a target without re-deriving the table shape:

type DataGridTableDndRowData = {
  type: "data-grid-row"
  depth: number
  index: number
  parentId: string | null
}
 
// in onDragOver / handleDragEnd
const active = event.active.data.current as DataGridTableDndRowData
const over = event.over?.data.current as DataGridTableDndRowData | undefined

Together these cover cross-parent (re-parenting) drops: drop restrictToVerticalAxis from modifiers so a horizontal gesture can express a depth change, track the intended parent and depth in onDragMove or onDragOver, paint the target with renderRowDecoration, and commit the move in handleDragEnd. The decoration node is positioned over the row, so it adds no column and does not disturb striping; give it absolute placement, for example an inset-x-0 bottom-0 h-0.5 bg-primary line offset by the target depth.


DataGridTableVirtual

A virtualized table renderer using @tanstack/react-virtual for row virtualization, infinite scroll, optional footer rows, and pinned rows when rowsPinnable is enabled. The wrapper manages row count and the scroll element for you, while virtualizerOptions lets you customize the underlying TanStack Virtual instance. Set scrollToRowIndex to reveal a controlled center row; "auto" alignment keeps already-visible rows in place and accounts for sticky headers.

PropTypeDefaultDescription
heightnumber | string-Optional fixed height when not using an outer scroll container.
estimateSizenumber48Estimated row height in pixels for the virtualizer.
overscannumber10Number of rows to render outside the visible area.
scrollBehaviorScrollBehavior"auto"Scroll animation used when revealing scrollToRowIndex.
scrollToRowAlign"auto" | "center" | "start" | "end""auto"Alignment used when revealing the target row. "auto" only scrolls when the row is outside the visible area.
scrollToRowIndexnumber-Index within the center (non-pinned) row section to reveal. Supports custom scroll elements and disabled virtualization.
footerContentReactNode-Optional footer content rendered inside <tfoot>.
renderHeaderbooleantrueWhether to render the table header.
onFetchMore() => void-Callback triggered when user scrolls near the bottom.
isFetchingMoreboolean-Whether additional data is currently being loaded.
hasMoreboolean-Whether there are more records available to fetch.
fetchMoreOffsetnumber0How many rows before the end should trigger onFetchMore.
virtualizerOptionsDataGridTableVirtualizerOptions<TData>-Optional passthrough for TanStack Virtual settings like enabled, getItemKey, measureElement, rangeExtractor, and onChange.

Set manualPagination: true. A virtualized grid renders every row, and dataGridFeatures registers paginatedRowModel, which is the one row model in the bundle that is not inert: table.getRowModel() is sliced to pageSize (default 10) unless the table opts out. Without it an otherwise correct virtual grid renders ten rows and stops. manualPagination: true is v9's way to say the data already is the page, so it keeps the pagination APIs while leaving the rows unsliced.


A pin/unpin toggle button for use in column definitions to enable row pinning.

PropTypeDefaultDescription
rowRow<DataGridFeatures, TData>-Required. The TanStack Table row instance.

DataGridFeatures is the type of the exported dataGridFeatures bundle. The button pins to the top region with row.pin("top") and clears it with row.pin(false). Turn pinning on with enableRowPinning on the table and tableLayout={{ rowsPinnable: true }} on DataGrid. In v9 both RowPinningState keys are required, so controlled state has to seed each region: useState<RowPinningState>({ top: [], bottom: [] }).


DataGridTableRowExpand

A depth-indented expand/collapse toggle for tree data, for use in the tree column's cell. Renders a chevron button for expandable rows (with aria-expanded reflecting state) and a compact spacer for leaves so leaf content sits close to the parent label.

PropTypeDefaultDescription
rowRow<DataGridFeatures, TData>-Required. The TanStack Table row instance.
indentnumber20Horizontal offset in px applied per tree depth level.
classNamestring-Additional CSS classes for the wrapper.
childrenReactNode-Custom toggle icon; replaces the default chevron.

Pass children to swap the default chevron for your own state-aware icon, for example {row.getIsExpanded() ? <FolderOpenIcon /> : <FolderIcon />}; the button always carries aria-expanded, so pure-CSS state styling keeps working. The wrapper exposes data-slot="data-grid-table-row-expand" and the computed --data-grid-tree-padding CSS variable as styling hooks. For fully custom cells, the exported getDataGridTreeIndentStyle(row, indent) helper returns the same indent style, and row.getCanExpand() / row.getIsExpanded() / row.getToggleExpandedHandler() cover bring-your-own toggles.


DataGridTableFoot

Wrapper component for the table footer (<tfoot>).


DataGridTableFootRow

A row inside the table footer.


DataGridTableFootRowCell

A cell inside a footer row.

PropTypeDefaultDescription
colSpannumber-Column span for the footer cell.
classNamestring-Additional CSS classes.
childrenReactNode-Content of the footer cell.

DOM Attributes

Data rows carry these attributes so you can target them from queries, tests, and styles. They are applied by the shared row renderer, so they appear on standard, virtualized, pinned, and draggable rows alike, with data-index the one exception (only DataGridTableVirtual sets it, and only on its unpinned body rows). Spacer, skeleton, empty, virtual status, and expanded-detail rows are rendered separately and carry none of them.

AttributeValueDescription
data-row-idstringThe resolved TanStack Table row id, respecting any getRowId configuration. Stable across sorting and pagination.
data-indexnumberIndex of the row inside the center (non-pinned) section. Set only by DataGridTableVirtual, for virtual measurement and stripe parity.
data-state"selected"Present while row selection is enabled and the row is selected. Omitted otherwise.
data-row-pinned"top" | "bottom"Which edge the row is pinned to. Omitted entirely when the row is not pinned.
data-row-pinned-boundary"top" | "bottom"Marks the seam between pinned and unpinned rows: the last top-pinned row, or the first bottom-pinned row.
data-depthnumberDepth of the row in a hierarchical row model (getSubRows trees or grouped rows). Omitted for root-level rows.

Header and body cells carry the pinning attributes below. They are the contract the grid's own sticky-column styling is built on, so they are also the hook to use for your own.

Renamed in v9

These values were left / right before TanStack Table v9. v9 moves column pinning to logical regions, and the grid follows it, so any CSS selecting [data-pinned="left"] needs updating to [data-pinned="start"].

AttributeValueDescription
data-pinned"start" | "end"Which edge the column is pinned to. Omitted entirely when the column is not pinned.
data-last-col"start" | "end"Marks the inner boundary of a pinned group - the last start-pinned or first end-pinned column. Carries the divider.
data-outer-pinned-col"start" | "end"Marks the outer edge of a pinned group. Header cells only; used for background clipping.

While a column is being resized the grid also renders a full-height vertical line at the pointer, with a cap in the header. It is only shown in onEnd mode - the default - because that is the mode where the column width does not move until you release, so the line is the only feedback the drag produces. Target it with [data-slot="data-grid-table-resize-indicator"].

It is positioned imperatively from a layout effect rather than through React state: a resize drag fires at pointer rate, and routing that through a render would re-render every row on every frame. The header height is measured once per drag session for the same reason.

Pinned cells stick with the CSS logical properties inset-inline-start and inset-inline-end rather than left / right, so they land on the correct edge in RTL without extra work.

"use client"

import { useMemo, useState } from "react"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
  ColumnDef,
  PaginationState,
  SortingState,
  useTable,
} from "@tanstack/react-table"

const users = [
  {
    id: "1",
    name: "Alex Johnson",
    email: "alex@example.com",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    initials: "AJ",
  },
  {
    id: "2",
    name: "Sarah Chen",
    email: "sarah@example.com",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    initials: "SC",
  },
  {
    id: "3",
    name: "Michael Rodriguez",
    email: "michael@example.com",
    avatar:
      "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
    initials: "MR",
  },
  {
    id: "4",
    name: "Emma Wilson",
    email: "emma@example.com",
    avatar:
      "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
    initials: "EW",
  },
  {
    id: "5",
    name: "David Kim",
    email: "david@example.com",
    avatar:
      "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
    initials: "DK",
  },
  {
    id: "6",
    name: "Aron Thompson",
    email: "lisa@example.com",
    avatar:
      "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
    initials: "LT",
  },
  {
    id: "7",
    name: "James Brown",
    email: "james@example.com",
    avatar:
      "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
    initials: "JB",
  },
  {
    id: "8",
    name: "Maria Garcia",
    email: "maria@example.com",
    avatar:
      "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
    initials: "MG",
  },
  {
    id: "9",
    name: "Nick Johnson",
    email: "nick@example.com",
    avatar:
      "https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80",
    initials: "NJ",
  },
  {
    id: "10",
    name: "Liam Thompson",
    email: "liam@example.com",
    avatar:
      "https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80",
    initials: "LT",
  },
]

interface IData {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string // Emoji flags
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const demoData: IData[] = users.map((user, index) => ({
  ...user,
  availability: (["online", "away", "busy", "offline"] as const)[index % 4],
  status: (index % 2 === 0 ? "active" : "inactive") as "active" | "inactive",
  flag: (["us", "gb", "ca", "au", "de", "my", "es", "jp", "fr", "it"] as const)[
    index % 10
  ],
  company: (
    [
      "Apple",
      "OpenAI",
      "Meta",
      "Tesla",
      "SAP",
      "Keenthemes",
      "BBVA",
      "Sony",
      "LVMH",
      "ENI",
    ] as const
  )[index % 10],
  role: (
    [
      "CEO",
      "CTO",
      "Designer",
      "Developer",
      "Lawyer",
      "Director",
      "Product Manager",
      "Marketing Lead",
      "Data Scientist",
      "Engineer",
    ] as const
  )[index % 10],
  joined: "Jan, 2024",
  location: (
    [
      "United States",
      "United Kingdom",
      "Canada",
      "Australia",
      "Germany",
      "Malaysia",
      "Spain",
      "Japan",
      "France",
      "Italy",
    ] as const
  )[index % 10],
  balance: 5143.03 + index * 100,
}))

export function Pattern() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 5,
  })
  const [sorting, setSorting] = useState<SortingState>([
    { id: "name", desc: true },
  ])

  const columns = useMemo<ColumnDef<DataGridFeatures, IData>[]>(
    () => [
      {
        accessorKey: "name",
        header: "Name",
        cell: (info) => <>{info.getValue() as string}</>,
        size: 150,
        meta: {
          headerClassName: "",
          cellClassName: "",
        },
      },
      {
        accessorKey: "email",
        header: "Email",
        cell: (info) => (
          <div className="truncate">
            <a
              href={`mailto:${info.getValue()}`}
              className="hover:text-primary truncate hover:underline"
            >
              {info.getValue() as string}
            </a>
          </div>
        ),
        size: 150,
        meta: {
          headerClassName: "",
          cellClassName: "",
        },
      },
      {
        accessorKey: "location",
        header: "Location",
        cell: ({ row }) => (
          <div className="flex items-center gap-1.5">
            <img
              src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
              alt={row.original.flag}
              className="size-4 rounded-full object-cover"
            />
            <div className="text-foreground">{row.original.location}</div>
          </div>
        ),
        size: 175,
        meta: {
          headerClassName: "",
          cellClassName: "",
        },
      },
      {
        accessorKey: "balance",
        header: "Balance ($)",
        cell: (info) => <>${(info.getValue() as number).toFixed(2)}</>,
        size: 100,
        meta: {
          headerClassName: "text-right rtl:text-left",
          cellClassName: "text-right rtl:text-left",
        },
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: demoData,
    pageCount: Math.ceil((demoData?.length || 0) / pagination.pageSize),
    getRowId: (row: IData) => row.id,
    state: {
      pagination,
      sorting,
    },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
  })

  return (
    <DataGrid table={table} recordCount={demoData?.length || 0}>
      <div className="w-full space-y-2.5">
        <DataGridContainer>
          <DataGridScrollArea>
            <DataGridTable />
          </DataGridScrollArea>
        </DataGridContainer>
        <DataGridPagination />
      </div>
    </DataGrid>
  )
}
"use client"

import { useMemo, useState } from "react"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
  ColumnDef,
  ColumnOrderState,
  PaginationState,
  SortingState,
  useTable,
} from "@tanstack/react-table"

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"
import { Card } from "@/components/ui/card"

interface IData {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string // Emoji flags
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const users = [
  {
    id: "1",
    name: "Alex Johnson",
    email: "alex@example.com",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    initials: "AJ",
  },
  {
    id: "2",
    name: "Sarah Chen",
    email: "sarah@example.com",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    initials: "SC",
  },
  {
    id: "3",
    name: "Michael Rodriguez",
    email: "michael@example.com",
    avatar:
      "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
    initials: "MR",
  },
  {
    id: "4",
    name: "Emma Wilson",
    email: "emma@example.com",
    avatar:
      "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
    initials: "EW",
  },
  {
    id: "5",
    name: "David Kim",
    email: "david@example.com",
    avatar:
      "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
    initials: "DK",
  },
  {
    id: "6",
    name: "Aron Thompson",
    email: "lisa@example.com",
    avatar:
      "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
    initials: "LT",
  },
  {
    id: "7",
    name: "James Brown",
    email: "james@example.com",
    avatar:
      "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
    initials: "JB",
  },
  {
    id: "8",
    name: "Maria Garcia",
    email: "maria@example.com",
    avatar:
      "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
    initials: "MG",
  },
  {
    id: "9",
    name: "Nick Johnson",
    email: "nick@example.com",
    avatar:
      "https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80",
    initials: "NJ",
  },
  {
    id: "10",
    name: "Liam Thompson",
    email: "liam@example.com",
    avatar:
      "https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80",
    initials: "LT",
  },
]

const demoData: IData[] = users.map((user, index) => ({
  ...user,
  availability: (["online", "away", "busy", "offline"] as const)[index % 4],
  status: (index % 2 === 0 ? "active" : "inactive") as "active" | "inactive",
  flag: (["us", "gb", "ca", "au", "de", "my", "es", "jp", "fr", "it"] as const)[
    index % 10
  ],
  company: (
    [
      "Apple",
      "OpenAI",
      "Meta",
      "Tesla",
      "SAP",
      "Keenthemes",
      "BBVA",
      "Sony",
      "LVMH",
      "ENI",
    ] as const
  )[index % 10],
  role: (
    [
      "CEO",
      "CTO",
      "Designer",
      "Developer",
      "Lawyer",
      "Director",
      "Product Manager",
      "Marketing Lead",
      "Data Scientist",
      "Engineer",
    ] as const
  )[index % 10],
  joined: "Jan, 2024",
  location: (
    [
      "United States",
      "United Kingdom",
      "Canada",
      "Australia",
      "Germany",
      "Malaysia",
      "Spain",
      "Japan",
      "France",
      "Italy",
    ] as const
  )[index % 10],
  balance: 5143.03 + index * 100,
}))

export function Pattern() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 5,
  })
  const [sorting, setSorting] = useState<SortingState>([
    { id: "name", desc: true },
  ])
  const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([])

  const columns = useMemo<ColumnDef<DataGridFeatures, IData>[]>(
    () => [
      {
        accessorKey: "name",
        id: "name",
        header: "Name",
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-3">
              <Avatar className="size-8">
                <AvatarImage
                  src={row.original.avatar}
                  alt={row.original.name}
                />
                <AvatarFallback>
                  {row.original.name
                    .split(" ")
                    .map((n) => n[0])
                    .join("")}
                </AvatarFallback>
              </Avatar>
              <div className="space-y-px">
                <div className="text-foreground font-medium">
                  {row.original.name}
                </div>
                <div className="text-muted-foreground">
                  {row.original.email}
                </div>
              </div>
            </div>
          )
        },
        size: 250,
        enableSorting: true,
        enableHiding: false,
      },
      {
        accessorKey: "company",
        header: "Company",
        cell: (info) => <span>{info.getValue() as string}</span>,
        size: 100,
        meta: {
          headerClassName: "",
        },
      },
      {
        accessorKey: "role",
        header: "Occupation",
        cell: (info) => <span>{info.getValue() as string}</span>,
        size: 100,
        meta: {
          headerClassName: "",
        },
      },
      {
        accessorKey: "balance",
        header: "Salary",
        cell: (info) => (
          <span className="font-semibold">
            ${(info.getValue() as number).toFixed(2)}
          </span>
        ),
        size: 100,
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: demoData,
    pageCount: Math.ceil((demoData?.length || 0) / pagination.pageSize),
    getRowId: (row: IData) => row.id,
    state: {
      pagination,
      sorting,
      columnOrder,
    },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
    onColumnOrderChange: setColumnOrder,
  })

  return (
    <DataGrid
      table={table}
      recordCount={demoData?.length || 0}
      tableLayout={{
        cellBorder: true,
      }}
    >
      <div className="w-full space-y-2.5">
        <Card className="p-0">
          <DataGridContainer>
            <DataGridScrollArea>
              <DataGridTable />
            </DataGridScrollArea>
          </DataGridContainer>
        </Card>
        <DataGridPagination />
      </div>
    </DataGrid>
  )
}
"use client"

import { useMemo, useState } from "react"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
  ColumnDef,
  PaginationState,
  SortingState,
  useTable,
} from "@tanstack/react-table"

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"

interface IData {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string // Emoji flags
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const users = [
  {
    id: "1",
    name: "Alex Johnson",
    email: "alex@example.com",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    initials: "AJ",
  },
  {
    id: "2",
    name: "Sarah Chen",
    email: "sarah@example.com",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    initials: "SC",
  },
  {
    id: "3",
    name: "Michael Rodriguez",
    email: "michael@example.com",
    avatar:
      "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
    initials: "MR",
  },
  {
    id: "4",
    name: "Emma Wilson",
    email: "emma@example.com",
    avatar:
      "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
    initials: "EW",
  },
  {
    id: "5",
    name: "David Kim",
    email: "david@example.com",
    avatar:
      "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
    initials: "DK",
  },
  {
    id: "6",
    name: "Aron Thompson",
    email: "lisa@example.com",
    avatar:
      "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
    initials: "LT",
  },
  {
    id: "7",
    name: "James Brown",
    email: "james@example.com",
    avatar:
      "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
    initials: "JB",
  },
  {
    id: "8",
    name: "Maria Garcia",
    email: "maria@example.com",
    avatar:
      "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
    initials: "MG",
  },
  {
    id: "9",
    name: "Nick Johnson",
    email: "nick@example.com",
    avatar:
      "https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80",
    initials: "NJ",
  },
  {
    id: "10",
    name: "Liam Thompson",
    email: "liam@example.com",
    avatar:
      "https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80",
    initials: "LT",
  },
]

const demoData: IData[] = users.map((user, index) => ({
  ...user,
  availability: (["online", "away", "busy", "offline"] as const)[index % 4],
  status: (index % 2 === 0 ? "active" : "inactive") as "active" | "inactive",
  flag: (["us", "gb", "ca", "au", "de", "my", "es", "jp", "fr", "it"] as const)[
    index % 10
  ],
  company: (
    [
      "Apple",
      "OpenAI",
      "Meta",
      "Tesla",
      "SAP",
      "Keenthemes",
      "BBVA",
      "Sony",
      "LVMH",
      "ENI",
    ] as const
  )[index % 10],
  role: (
    [
      "CEO",
      "CTO",
      "Designer",
      "Developer",
      "Lawyer",
      "Director",
      "Product Manager",
      "Marketing Lead",
      "Data Scientist",
      "Engineer",
    ] as const
  )[index % 10],
  joined: "Jan, 2024",
  location: (
    [
      "United States",
      "United Kingdom",
      "Canada",
      "Australia",
      "Germany",
      "Malaysia",
      "Spain",
      "Japan",
      "France",
      "Italy",
    ] as const
  )[index % 10],
  balance: 5143.03 + index * 100,
}))

export function Pattern() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 5,
  })

  const [sorting, setSorting] = useState<SortingState>([
    { id: "name", desc: true },
  ])

  const columns = useMemo<ColumnDef<DataGridFeatures, IData>[]>(
    () => [
      {
        accessorKey: "name",
        id: "name",
        header: "Name",
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-2">
              <Avatar className="size-6">
                <AvatarImage
                  src={row.original.avatar}
                  alt={row.original.name}
                />
                <AvatarFallback>
                  {row.original.name
                    .split(" ")
                    .map((n) => n[0])
                    .join("")}
                </AvatarFallback>
              </Avatar>
              <a
                href="#"
                className="text-foreground hover:text-primary font-medium"
              >
                {row.original.name}
              </a>
            </div>
          )
        },
        size: 200,
        enableSorting: true,
        enableHiding: false,
      },
      {
        accessorKey: "email",
        header: "Email",
        cell: (info) => (
          <a
            href={`mailto:${info.getValue()}`}
            className="hover:text-primary hover:underline"
          >
            {info.getValue() as string}
          </a>
        ),
        size: 175,
        meta: {
          headerClassName: "",
        },
      },
      {
        accessorKey: "location",
        header: "Location",
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-1.5">
              <img
                src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
                alt={row.original.flag}
                className="size-4 rounded-full object-cover"
              />
              <div className="text-foreground font-medium">
                {row.original.location}
              </div>
            </div>
          )
        },
        size: 175,
        meta: {
          headerClassName: "",
          cellClassName: "text-start",
        },
      },
      {
        accessorKey: "balance",
        header: "Balance ($)",
        cell: (info) => (
          <span className="font-semibold">
            ${(info.getValue() as number).toFixed(2)}
          </span>
        ),
        size: 125,
        meta: {
          headerClassName: "text-right rtl:text-left",
          cellClassName: "text-right rtl:text-left",
        },
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: demoData,
    pageCount: Math.ceil((demoData?.length || 0) / pagination.pageSize),
    getRowId: (row: IData) => row.id,
    state: {
      pagination,
      sorting,
    },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
  })

  return (
    <DataGrid
      table={table}
      recordCount={demoData?.length || 0}
      tableLayout={{ dense: true }}
    >
      <div className="w-full space-y-2.5">
        <DataGridContainer>
          <DataGridScrollArea>
            <DataGridTable />
          </DataGridScrollArea>
        </DataGridContainer>
        <DataGridPagination />
      </div>
    </DataGrid>
  )
}
"use client"

import { useMemo, useState } from "react"
import { Badge } from "@/components/reui/badge"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
  ColumnDef,
  PaginationState,
  SortingState,
  useTable,
} from "@tanstack/react-table"

import { cn } from "@/lib/utils"
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"

interface IData {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string // Emoji flags
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const demoData: IData[] = [
  {
    id: "1",
    name: "Alex Johnson",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "us",
    email: "alex@apple.com",
    company: "Apple",
    role: "CEO",
    joined: "Jan, 2024",
    location: "United States",
    balance: 5143.03,
  },
  {
    id: "2",
    name: "Sarah Chen",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "gb",
    email: "sarah@openai.com",
    company: "OpenAI",
    role: "CTO",
    joined: "Mar, 2023",
    location: "United Kingdom",
    balance: 4321.87,
  },
  {
    id: "3",
    name: "Michael Rodriguez",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "ca",
    email: "michael@meta.com",
    company: "Meta",
    role: "Designer",
    joined: "Jun, 2022",
    location: "Canada",
    balance: 7654.98,
  },
  {
    id: "4",
    name: "Emma Wilson",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "au",
    email: "emma@tesla.com",
    company: "Tesla",
    role: "Developer",
    joined: "Sep, 2024",
    location: "Australia",
    balance: 3456.45,
  },
  {
    id: "5",
    name: "David Kim",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "de",
    email: "david@sap.com",
    company: "SAP",
    role: "Lawyer",
    joined: "Nov, 2023",
    location: "Germany",
    balance: 9876.54,
  },
  {
    id: "6",
    name: "Aron Thompson",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "my",
    email: "aron@keenthemes.com",
    company: "Keenthemes",
    role: "Director",
    joined: "Feb, 2022",
    location: "Malaysia",
    balance: 6214.22,
  },
  {
    id: "7",
    name: "James Brown",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "es",
    email: "james@bbva.es",
    company: "BBVA",
    role: "Product Manager",
    joined: "Aug, 2024",
    location: "Spain",
    balance: 5321.77,
  },
  {
    id: "8",
    name: "Maria Garcia",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "jp",
    email: "maria@sony.jp",
    company: "Sony",
    role: "Marketing Lead",
    joined: "Dec, 2023",
    location: "Japan",
    balance: 8452.39,
  },
  {
    id: "9",
    name: "Nick Johnson",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "fr",
    email: "nick@lvmh.fr",
    company: "LVMH",
    role: "Data Scientist",
    joined: "Apr, 2022",
    location: "France",
    balance: 7345.1,
  },
  {
    id: "10",
    name: "Liam Thompson",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "it",
    email: "liam@eni.it",
    company: "ENI",
    role: "Engineer",
    joined: "Jul, 2024",
    location: "Italy",
    balance: 5214.88,
  },
  {
    id: "11",
    name: "Alex Johnson",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "br",
    email: "alex@vale.br",
    company: "Vale",
    role: "Software Engineer",
    joined: "May, 2023",
    location: "Brazil",
    balance: 9421.5,
  },
  {
    id: "12",
    name: "Sarah Chen",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "in",
    email: "sarah@tata.in",
    company: "Tata",
    role: "Sales Manager",
    joined: "Oct, 2024",
    location: "India",
    balance: 4521.67,
  },
]

export function Pattern() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 5,
  })
  const [sorting, setSorting] = useState<SortingState>([
    { id: "name", desc: true },
  ])

  const columns = useMemo<ColumnDef<DataGridFeatures, IData>[]>(
    () => [
      {
        accessorKey: "name",
        id: "name",
        header: "Name",
        cell: ({ row }) => {
          const availability = row.original.availability
          const statusColors = {
            online: "bg-green-500",
            away: "bg-yellow-500",
            busy: "bg-orange-500",
            offline: "bg-gray-400",
          }

          return (
            <div className="flex items-center gap-3">
              <Avatar className="size-8">
                <AvatarImage
                  src={row.original.avatar}
                  alt={row.original.name}
                />
                <AvatarFallback>
                  {row.original.name
                    .split(" ")
                    .map((n) => n[0])
                    .join("")}
                </AvatarFallback>
                <AvatarBadge
                  className={cn(
                    "size-1.5! p-0",
                    statusColors[availability] || statusColors.offline
                  )}
                />
              </Avatar>
              <div className="space-y-px">
                <div className="text-foreground font-medium">
                  {row.original.name}
                </div>
                <div className="text-muted-foreground">
                  {row.original.email}
                </div>
              </div>
            </div>
          )
        },
        size: 225,
        enableSorting: true,
        enableHiding: false,
      },
      {
        accessorKey: "location",
        header: "Location",
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-1.5">
              <img
                src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
                alt={row.original.flag}
                className="size-4 rounded-full object-cover"
              />
              <div className="text-foreground font-medium">
                {row.original.location}
              </div>
            </div>
          )
        },
        size: 160,
        meta: {
          headerClassName: "",
          cellClassName: "text-start",
        },
      },
      {
        accessorKey: "status",
        id: "status",
        header: "Status",
        cell: ({ row }) => {
          const status = row.original.status

          if (status == "active") {
            return <Badge variant="success-outline">Approved</Badge>
          } else {
            return <Badge variant="warning-outline">Pending</Badge>
          }
        },
        size: 100,
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: demoData,
    pageCount: Math.ceil((demoData?.length || 0) / pagination.pageSize),
    getRowId: (row: IData) => row.id,
    state: {
      pagination,
      sorting,
    },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
  })

  return (
    <DataGrid
      table={table}
      recordCount={demoData?.length || 0}
      tableLayout={{
        headerBackground: false,
        rowBorder: false,
        rowRounded: true,
      }}
    >
      <div className="w-full space-y-2.5">
        <DataGridContainer>
          <DataGridScrollArea>
            <DataGridTable />
          </DataGridScrollArea>
        </DataGridContainer>
        <DataGridPagination />
      </div>
    </DataGrid>
  )
}
"use client"

import { useMemo, useState } from "react"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
  ColumnDef,
  PaginationState,
  SortingState,
  useTable,
} from "@tanstack/react-table"

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"

interface IData {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string // Emoji flags
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const demoData: IData[] = [
  {
    id: "1",
    name: "Alex Johnson",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "us",
    email: "alex@apple.com",
    company: "Apple",
    role: "CEO",
    joined: "Jan, 2024",
    location: "United States",
    balance: 5143.03,
  },
  {
    id: "2",
    name: "Sarah Chen",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "gb",
    email: "sarah@openai.com",
    company: "OpenAI",
    role: "CTO",
    joined: "Mar, 2023",
    location: "United Kingdom",
    balance: 4321.87,
  },
  {
    id: "3",
    name: "Michael Rodriguez",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "ca",
    email: "michael@meta.com",
    company: "Meta",
    role: "Designer",
    joined: "Jun, 2022",
    location: "Canada",
    balance: 7654.98,
  },
  {
    id: "4",
    name: "Emma Wilson",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "au",
    email: "emma@tesla.com",
    company: "Tesla",
    role: "Developer",
    joined: "Sep, 2024",
    location: "Australia",
    balance: 3456.45,
  },
  {
    id: "5",
    name: "David Kim",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "de",
    email: "david@sap.com",
    company: "SAP",
    role: "Lawyer",
    joined: "Nov, 2023",
    location: "Germany",
    balance: 9876.54,
  },
  {
    id: "6",
    name: "Aron Thompson",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "my",
    email: "aron@keenthemes.com",
    company: "Keenthemes",
    role: "Director",
    joined: "Feb, 2022",
    location: "Malaysia",
    balance: 6214.22,
  },
  {
    id: "7",
    name: "James Brown",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "es",
    email: "james@bbva.es",
    company: "BBVA",
    role: "Product Manager",
    joined: "Aug, 2024",
    location: "Spain",
    balance: 5321.77,
  },
  {
    id: "8",
    name: "Maria Garcia",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "jp",
    email: "maria@sony.jp",
    company: "Sony",
    role: "Marketing Lead",
    joined: "Dec, 2023",
    location: "Japan",
    balance: 8452.39,
  },
  {
    id: "9",
    name: "Nick Johnson",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "fr",
    email: "nick@lvmh.fr",
    company: "LVMH",
    role: "Data Scientist",
    joined: "Apr, 2022",
    location: "France",
    balance: 7345.1,
  },
  {
    id: "10",
    name: "Liam Thompson",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "it",
    email: "liam@eni.it",
    company: "ENI",
    role: "Engineer",
    joined: "Jul, 2024",
    location: "Italy",
    balance: 5214.88,
  },
  {
    id: "11",
    name: "Alex Johnson",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "br",
    email: "alex@vale.br",
    company: "Vale",
    role: "Software Engineer",
    joined: "May, 2023",
    location: "Brazil",
    balance: 9421.5,
  },
  {
    id: "12",
    name: "Sarah Chen",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "in",
    email: "sarah@tata.in",
    company: "Tata",
    role: "Sales Manager",
    joined: "Oct, 2024",
    location: "India",
    balance: 4521.67,
  },
]

export function Pattern() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 5,
  })
  const [sorting, setSorting] = useState<SortingState>([
    { id: "name", desc: true },
  ])

  const columns = useMemo<ColumnDef<DataGridFeatures, IData>[]>(
    () => [
      {
        accessorKey: "name",
        id: "name",
        header: "Name",
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-2">
              <Avatar className="size-6">
                <AvatarImage
                  src={row.original.avatar}
                  alt={row.original.name}
                />
                <AvatarFallback>
                  {row.original.name
                    .split(" ")
                    .map((n) => n[0])
                    .join("")}
                </AvatarFallback>
              </Avatar>
              <a
                href="#"
                className="text-foreground hover:text-primary font-medium"
              >
                {row.original.name}
              </a>
            </div>
          )
        },
        size: 175,
        enableSorting: true,
        enableHiding: false,
      },
      {
        accessorKey: "email",
        header: "Email",
        cell: (info) => (
          <a
            href={`mailto:${info.getValue()}`}
            className="hover:text-primary hover:underline"
          >
            {info.getValue() as string}
          </a>
        ),
        size: 180,
      },
      {
        accessorKey: "location",
        header: "Location",
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-1.5">
              <img
                src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
                alt={row.original.flag}
                className="size-4 rounded-full object-cover"
              />
              <div className="text-foreground font-medium">
                {row.original.location}
              </div>
            </div>
          )
        },
        size: 170,
      },
      {
        accessorKey: "balance",
        header: "Balance ($)",
        cell: (info) => (
          <span className="font-semibold">
            ${(info.getValue() as number).toFixed(2)}
          </span>
        ),
        size: 120,
        meta: {
          headerClassName: "text-right rtl:text-left",
          cellClassName: "text-right rtl:text-left",
        },
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: demoData,
    pageCount: Math.ceil((demoData?.length || 0) / pagination.pageSize),
    getRowId: (row: IData) => row.id,
    state: {
      pagination,
      sorting,
    },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
  })

  return (
    <DataGrid
      table={table}
      recordCount={demoData?.length || 0}
      tableLayout={{
        stripped: true,
        rowRounded: true,
      }}
    >
      <div className="w-full space-y-2.5">
        <DataGridContainer>
          <DataGridScrollArea>
            <DataGridTable />
          </DataGridScrollArea>
        </DataGridContainer>
        <DataGridPagination />
      </div>
    </DataGrid>
  )
}
"use client"

import { useMemo, useState } from "react"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
  ColumnDef,
  PaginationState,
  SortingState,
  useTable,
} from "@tanstack/react-table"

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"

interface Data {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string // Emoji flags
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const demoData: Data[] = [
  {
    id: "1",
    name: "Alex Johnson",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "us",
    email: "alex@apple.com",
    company: "Apple",
    role: "CEO",
    joined: "Jan, 2026",
    location: "United States",
    balance: 5143.03,
  },
  {
    id: "2",
    name: "Sarah Chen",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "gb",
    email: "sarah@openai.com",
    company: "OpenAI",
    role: "CTO",
    joined: "Jul, 2025",
    location: "United Kingdom",
    balance: 4321.87,
  },
  {
    id: "3",
    name: "Michael Rodriguez",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "ca",
    email: "michael@meta.com",
    company: "Meta",
    role: "Designer",
    joined: "Mar, 2019",
    location: "Canada",
    balance: 7654.98,
  },
  {
    id: "4",
    name: "Emma Wilson",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "au",
    email: "emma@tesla.com",
    company: "Tesla",
    role: "Developer",
    joined: "Jan, 2024",
    location: "Australia",
    balance: 3456.45,
  },
  {
    id: "5",
    name: "David Kim",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "de",
    email: "david@sap.com",
    company: "SAP",
    role: "Lawyer",
    joined: "May, 2023",
    location: "Germany",
    balance: 9876.54,
  },
  {
    id: "6",
    name: "Aron Thompson",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "my",
    email: "aron@keenthemes.com",
    company: "Keenthemes",
    role: "Director",
    joined: "Nov, 2018",
    location: "Malaysia",
    balance: 6214.22,
  },
  {
    id: "7",
    name: "James Brown",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "es",
    email: "james@bbva.es",
    company: "BBVA",
    role: "Product Manager",
    joined: "Jun, 2021",
    location: "Spain",
    balance: 5321.77,
  },
  {
    id: "8",
    name: "Maria Garcia",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "jp",
    email: "maria@sony.jp",
    company: "Sony",
    role: "Marketing Lead",
    joined: "Oct, 2020",
    location: "Japan",
    balance: 8452.39,
  },
  {
    id: "9",
    name: "Nick Johnson",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "fr",
    email: "nick@lvmh.fr",
    company: "LVMH",
    role: "Data Scientist",
    joined: "Sep, 2019",
    location: "France",
    balance: 7345.1,
  },
  {
    id: "10",
    name: "Liam Thompson",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "it",
    email: "liam@eni.it",
    company: "ENI",
    role: "Engineer",
    joined: "Feb, 2023",
    location: "Italy",
    balance: 5214.88,
  },
  {
    id: "11",
    name: "Alex Johnson",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "br",
    email: "alex@vale.br",
    company: "Vale",
    role: "Software Engineer",
    joined: "Dec, 2022",
    location: "Brazil",
    balance: 9421.5,
  },
  {
    id: "12",
    name: "Sarah Chen",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "in",
    email: "sarah@tata.in",
    company: "Tata",
    role: "Sales Manager",
    joined: "Mar, 2020",
    location: "India",
    balance: 4521.67,
  },
]

export function Pattern() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 5,
  })
  const [sorting, setSorting] = useState<SortingState>([
    { id: "name", desc: true },
  ])

  const columns = useMemo<ColumnDef<DataGridFeatures, Data>[]>(
    () => [
      {
        accessorKey: "name",
        id: "name",
        header: "Name",
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-2">
              <Avatar className="size-6">
                <AvatarImage
                  src={row.original.avatar}
                  alt={row.original.name}
                />
                <AvatarFallback>
                  {row.original.name
                    .split(" ")
                    .map((n) => n[0])
                    .join("")}
                </AvatarFallback>
              </Avatar>
              <a
                href="#"
                className="text-foreground hover:text-primary font-medium"
              >
                {row.original.name}
              </a>
            </div>
          )
        },
        size: 225,
        enableSorting: true,
        enableHiding: false,
      },
      {
        accessorKey: "email",
        header: "Email",
        cell: (info) => (
          <a
            href={`mailto:${info.getValue()}`}
            className="hover:text-primary hover:underline"
          >
            {info.getValue() as string}
          </a>
        ),
        size: 200,
      },
      {
        accessorKey: "location",
        header: "Location",
        cell: ({ row }) => (
          <div className="flex items-center gap-1.5">
            <img
              src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
              alt={row.original.flag}
              className="size-4 rounded-full object-cover"
            />
            <div className="text-foreground font-medium">
              {row.original.location}
            </div>
          </div>
        ),
        size: 175,
      },
      {
        accessorKey: "joined",
        header: "Joined",
        cell: (info) => info.getValue() as string,
        size: 120,
        meta: {
          cellClassName: "font-medium",
        },
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: demoData,
    pageCount: Math.ceil((demoData?.length || 0) / pagination.pageSize),
    getRowId: (row: Data) => row.id,
    state: {
      pagination,
      sorting,
    },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
  })

  return (
    <DataGrid
      table={table}
      recordCount={demoData?.length || 0}
      tableLayout={{
        width: "auto",
      }}
    >
      <div className="w-full space-y-2.5">
        <DataGridContainer>
          <DataGridScrollArea>
            <DataGridTable />
          </DataGridScrollArea>
        </DataGridContainer>
        <DataGridPagination />
      </div>
    </DataGrid>
  )
}
"use client"

import { useEffect, useMemo, useState } from "react"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import {
  DataGridTable,
  DataGridTableRowSelect,
  DataGridTableRowSelectAll,
} from "@/components/reui/data-grid/data-grid-table"
import {
  ColumnDef,
  PaginationState,
  RowSelectionState,
  SortingState,
  useTable,
} from "@tanstack/react-table"

import { cn } from "@/lib/utils"
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"

interface IData {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const demoData: IData[] = [
  {
    id: "1",
    name: "Alex Johnson",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "us",
    email: "alex@apple.com",
    company: "Apple",
    role: "CEO",
    joined: "Apr, 2021",
    location: "United States",
    balance: 5143.03,
  },
  {
    id: "2",
    name: "Sarah Chen",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "gb",
    email: "sarah@openai.com",
    company: "OpenAI",
    role: "CTO",
    joined: "Jul, 2020",
    location: "United Kingdom",
    balance: 4321.87,
  },
  {
    id: "3",
    name: "Michael Rodriguez",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "ca",
    email: "michael@meta.com",
    company: "Meta",
    role: "Designer",
    joined: "Mar, 2019",
    location: "Canada",
    balance: 7654.98,
  },
  {
    id: "4",
    name: "Emma Wilson",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "au",
    email: "emma@tesla.com",
    company: "Tesla",
    role: "Developer",
    joined: "Jan, 2022",
    location: "Australia",
    balance: 3456.45,
  },
  {
    id: "5",
    name: "David Kim",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "de",
    email: "david@sap.com",
    company: "SAP",
    role: "Lawyer",
    joined: "May, 2023",
    location: "Germany",
    balance: 9876.54,
  },
  {
    id: "6",
    name: "Aron Thompson",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "my",
    email: "aron@keenthemes.com",
    company: "Keenthemes",
    role: "Director",
    joined: "Nov, 2018",
    location: "Malaysia",
    balance: 6214.22,
  },
  {
    id: "7",
    name: "James Brown",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "es",
    email: "james@bbva.es",
    company: "BBVA",
    role: "Product Manager",
    joined: "Jun, 2021",
    location: "Spain",
    balance: 5321.77,
  },
  {
    id: "8",
    name: "Maria Garcia",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "jp",
    email: "maria@sony.jp",
    company: "Sony",
    role: "Marketing Lead",
    joined: "Oct, 2020",
    location: "Japan",
    balance: 8452.39,
  },
  {
    id: "9",
    name: "Nick Johnson",
    availability: "online",
    avatar:
      "https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "fr",
    email: "nick@lvmh.fr",
    company: "LVMH",
    role: "Data Scientist",
    joined: "Sep, 2019",
    location: "France",
    balance: 7345.1,
  },
  {
    id: "10",
    name: "Liam Thompson",
    availability: "away",
    avatar:
      "https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80",
    status: "inactive",
    flag: "it",
    email: "liam@eni.it",
    company: "ENI",
    role: "Engineer",
    joined: "Feb, 2023",
    location: "Italy",
    balance: 5214.88,
  },
  {
    id: "11",
    name: "Alex Johnson",
    availability: "busy",
    avatar:
      "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "br",
    email: "alex@vale.br",
    company: "Vale",
    role: "Software Engineer",
    joined: "Dec, 2022",
    location: "Brazil",
    balance: 9421.5,
  },
  {
    id: "12",
    name: "Sarah Chen",
    availability: "offline",
    avatar:
      "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
    status: "active",
    flag: "in",
    email: "sarah@tata.in",
    company: "Tata",
    role: "Sales Manager",
    joined: "Mar, 2020",
    location: "India",
    balance: 4521.67,
  },
]

export function Pattern() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 5,
  })
  const [sorting, setSorting] = useState<SortingState>([
    { id: "name", desc: true },
  ])
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const [selectedIds, setSelectedIds] = useState<string[]>([])

  useEffect(() => {
    const selectedRowIds = Object.keys(rowSelection)
    if (selectedRowIds.length > 0) {
      setSelectedIds(selectedRowIds)
    } else {
      setSelectedIds([])
    }
  }, [rowSelection])

  const columns = useMemo<ColumnDef<DataGridFeatures, IData>[]>(
    () => [
      {
        accessorKey: "id",
        header: () => <DataGridTableRowSelectAll />,
        cell: ({ row }) => <DataGridTableRowSelect row={row} />,
        enableSorting: false,
        size: 20,
        meta: {
          headerClassName: "",
          cellClassName: "",
        },
      },
      {
        accessorKey: "name",
        id: "name",
        header: "Name",
        cell: ({ row }) => {
          const availability = row.original.availability
          const statusColors = {
            online: "bg-green-500",
            away: "bg-yellow-500",
            busy: "bg-orange-500",
            offline: "bg-gray-400",
          }

          return (
            <div className="flex items-center gap-3">
              <Avatar className="size-8">
                <AvatarImage
                  src={row.original.avatar}
                  alt={row.original.name}
                />
                <AvatarFallback>
                  {row.original.name
                    .split(" ")
                    .map((n) => n[0])
                    .join("")}
                </AvatarFallback>
                <AvatarBadge
                  className={cn(
                    "size-1.5! p-0",
                    statusColors[availability] || statusColors.offline
                  )}
                />
              </Avatar>
              <div className="space-y-px">
                <div className="text-foreground font-medium">
                  {row.original.name}
                </div>
                <div className="text-muted-foreground">
                  {row.original.email}
                </div>
              </div>
            </div>
          )
        },
        size: 200,
        enableSorting: true,
        enableHiding: false,
      },
      {
        accessorKey: "location",
        header: "Location",
        cell: ({ row }) => {
          return (
            <div className="flex items-center gap-1.5">
              <img
                src={`https://flagcdn.com/${row.original.flag.toLowerCase()}.svg`}
                alt={row.original.flag}
                className="size-4 rounded-full object-cover"
              />
              <div className="text-foreground font-medium">
                {row.original.location}
              </div>
            </div>
          )
        },
        size: 180,
        meta: {
          headerClassName: "",
          cellClassName: "text-start",
        },
      },
      {
        accessorKey: "joined",
        header: "Joined",
        cell: (info) => info.getValue() as string,
        size: 120,
        meta: {
          headerClassName: "",
          cellClassName: "font-medium",
        },
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: demoData,
    pageCount: Math.ceil((demoData?.length || 0) / pagination.pageSize),
    getRowId: (row: IData) => row.id,
    state: {
      pagination,
      sorting,
      rowSelection,
    },
    enableRowSelection: true,
    onRowSelectionChange: setRowSelection,
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
  })

  return (
    <DataGrid table={table} recordCount={demoData?.length || 0}>
      <div className="w-full space-y-2.5">
        <DataGridContainer>
          <DataGridScrollArea>
            <DataGridTable />
          </DataGridScrollArea>
        </DataGridContainer>
        <DataGridPagination />
      </div>
    </DataGrid>
  )
}
"use client"

import { useEffect, useMemo, useState } from "react"
import { Badge } from "@/components/reui/badge"
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
  type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import { DataGridPagination } from "@/components/reui/data-grid/data-grid-pagination"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import {
  DataGridTable,
  DataGridTableRowExpand,
} from "@/components/reui/data-grid/data-grid-table"
import {
  ColumnDef,
  ExpandedState,
  PaginationState,
  SortingState,
  useTable,
} from "@tanstack/react-table"

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"
import { Card } from "@/components/ui/card"

interface IData {
  id: string
  name: string
  type: "department" | "team" | "member"
  status: "active" | "inactive"
  role?: string
  avatar?: string
  flag?: string
  location?: string
  children?: IData[]
}

const demoData: IData[] = [
  {
    id: "eng",
    name: "Engineering",
    type: "department",
    status: "active",
    children: [
      {
        id: "eng-platform",
        name: "Platform",
        type: "team",
        status: "active",
        children: [
          {
            id: "eng-platform-1",
            name: "Alex Johnson",
            type: "member",
            status: "active",
            role: "Staff Engineer",
            avatar:
              "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
            flag: "us",
            location: "United States",
          },
          {
            id: "eng-platform-2",
            name: "Sarah Chen",
            type: "member",
            status: "active",
            role: "Senior Engineer",
            avatar:
              "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
            flag: "gb",
            location: "United Kingdom",
          },
          {
            id: "eng-platform-3",
            name: "Michael Rodriguez",
            type: "member",
            status: "inactive",
            role: "Frontend Engineer",
            avatar:
              "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
            flag: "ca",
            location: "Canada",
          },
        ],
      },
      {
        id: "eng-mobile",
        name: "Mobile",
        type: "team",
        status: "active",
        children: [
          {
            id: "eng-mobile-1",
            name: "Emma Wilson",
            type: "member",
            status: "active",
            role: "iOS Engineer",
            avatar:
              "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
            flag: "au",
            location: "Australia",
          },
          {
            id: "eng-mobile-2",
            name: "David Kim",
            type: "member",
            status: "active",
            role: "Android Engineer",
            avatar:
              "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
            flag: "de",
            location: "Germany",
          },
        ],
      },
    ],
  },
  {
    id: "design",
    name: "Design",
    type: "department",
    status: "active",
    children: [
      {
        id: "design-product",
        name: "Product Design",
        type: "team",
        status: "active",
        children: [
          {
            id: "design-product-1",
            name: "Aron Thompson",
            type: "member",
            status: "active",
            role: "Design Lead",
            avatar:
              "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
            flag: "my",
            location: "Malaysia",
          },
          {
            id: "design-product-2",
            name: "Maria Garcia",
            type: "member",
            status: "active",
            role: "Product Designer",
            avatar:
              "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
            flag: "jp",
            location: "Japan",
          },
          {
            id: "design-product-3",
            name: "James Brown",
            type: "member",
            status: "inactive",
            role: "UX Researcher",
            avatar:
              "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
            flag: "es",
            location: "Spain",
          },
        ],
      },
      {
        id: "design-brand",
        name: "Brand",
        type: "team",
        status: "active",
        children: [
          {
            id: "design-brand-1",
            name: "Nick Johnson",
            type: "member",
            status: "active",
            role: "Brand Designer",
            avatar:
              "https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80",
            flag: "fr",
            location: "France",
          },
          {
            id: "design-brand-2",
            name: "Liam Thompson",
            type: "member",
            status: "inactive",
            role: "Motion Designer",
            avatar:
              "https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80",
            flag: "it",
            location: "Italy",
          },
        ],
      },
    ],
  },
  {
    id: "marketing",
    name: "Marketing",
    type: "department",
    status: "active",
    children: [
      {
        id: "marketing-growth",
        name: "Growth",
        type: "team",
        status: "active",
        children: [
          {
            id: "marketing-growth-1",
            name: "Olivia Martin",
            type: "member",
            status: "active",
            role: "Growth Lead",
            avatar:
              "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
            flag: "us",
            location: "United States",
          },
          {
            id: "marketing-growth-2",
            name: "Ethan Clark",
            type: "member",
            status: "active",
            role: "Performance Marketer",
            avatar:
              "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
            flag: "ca",
            location: "Canada",
          },
        ],
      },
      {
        id: "marketing-content",
        name: "Content",
        type: "team",
        status: "active",
        children: [
          {
            id: "marketing-content-1",
            name: "Sofia Rossi",
            type: "member",
            status: "active",
            role: "Content Lead",
            avatar:
              "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
            flag: "it",
            location: "Italy",
          },
          {
            id: "marketing-content-2",
            name: "Lucas Meyer",
            type: "member",
            status: "inactive",
            role: "Copywriter",
            avatar:
              "https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
            flag: "de",
            location: "Germany",
          },
        ],
      },
    ],
  },
  {
    id: "operations",
    name: "Operations",
    type: "department",
    status: "active",
    children: [
      {
        id: "operations-finance",
        name: "Finance",
        type: "team",
        status: "active",
        children: [
          {
            id: "operations-finance-1",
            name: "Grace Lee",
            type: "member",
            status: "active",
            role: "Finance Lead",
            avatar:
              "https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80",
            flag: "kr",
            location: "South Korea",
          },
          {
            id: "operations-finance-2",
            name: "Daniel Novak",
            type: "member",
            status: "active",
            role: "Accountant",
            avatar:
              "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
            flag: "cz",
            location: "Czechia",
          },
        ],
      },
      {
        id: "operations-people",
        name: "People",
        type: "team",
        status: "active",
        children: [
          {
            id: "operations-people-1",
            name: "Chloe Dubois",
            type: "member",
            status: "active",
            role: "People Lead",
            avatar:
              "https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
            flag: "fr",
            location: "France",
          },
          {
            id: "operations-people-2",
            name: "Ryan Walsh",
            type: "member",
            status: "active",
            role: "Recruiter",
            avatar:
              "https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80",
            flag: "ie",
            location: "Ireland",
          },
        ],
      },
    ],
  },
  {
    id: "sales",
    name: "Sales",
    type: "department",
    status: "active",
    children: [
      {
        id: "sales-accounts",
        name: "Accounts",
        type: "team",
        status: "active",
        children: [
          {
            id: "sales-accounts-1",
            name: "Mia Park",
            type: "member",
            status: "active",
            role: "Account Executive",
            avatar:
              "https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80",
            flag: "kr",
            location: "South Korea",
          },
          {
            id: "sales-accounts-2",
            name: "Noah Fischer",
            type: "member",
            status: "inactive",
            role: "Account Manager",
            avatar:
              "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
            flag: "at",
            location: "Austria",
          },
        ],
      },
    ],
  },
]

// Collapsed rows are unmounted, so their images would load only when a
// branch is first expanded and pop in after the fallback renders. Warming
// them once at mount keeps expansion flicker-free.
function collectImageUrls(rows: IData[]): string[] {
  return rows.flatMap((row) => [
    ...(row.avatar ? [row.avatar] : []),
    ...(row.flag ? [`https://flagcdn.com/${row.flag.toLowerCase()}.svg`] : []),
    ...(row.children ? collectImageUrls(row.children) : []),
  ])
}

export function Pattern() {
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: 4,
  })
  const [sorting, setSorting] = useState<SortingState>([])
  const [expanded, setExpanded] = useState<ExpandedState>({
    eng: true,
    "eng-platform": true,
  })

  useEffect(() => {
    for (const src of collectImageUrls(demoData)) {
      const image = new Image()
      image.src = src
    }
  }, [])

  const columns = useMemo<ColumnDef<DataGridFeatures, IData>[]>(
    () => [
      {
        accessorKey: "name",
        id: "name",
        header: ({ column }) => (
          <DataGridColumnHeader title="Name" column={column} />
        ),
        cell: ({ row }) => {
          const item = row.original

          return (
            <div className="flex items-center gap-1">
              <DataGridTableRowExpand row={row} className="-ms-1.5 -me-1" />
              {item.type === "member" ? (
                <>
                  <Avatar className="size-6 shrink-0">
                    <AvatarImage src={item.avatar} alt={item.name} />
                    <AvatarFallback>
                      {item.name
                        .split(" ")
                        .map((n) => n[0])
                        .join("")}
                    </AvatarFallback>
                  </Avatar>
                  <a
                    href="#"
                    className="text-foreground hover:text-primary font-medium"
                  >
                    {item.name}
                  </a>
                </>
              ) : (
                <span className="text-foreground font-medium">{item.name}</span>
              )}
            </div>
          )
        },
        minSize: 260,
        enableSorting: true,
        enableHiding: false,
        meta: {
          autoSize: true,
        },
      },
      {
        accessorKey: "role",
        header: ({ column }) => (
          <DataGridColumnHeader title="Role" column={column} />
        ),
        cell: ({ row }) => {
          const item = row.original

          return (
            <div className="text-muted-foreground">
              {item.role ??
                (item.type === "department" ? "Department" : "Team")}
            </div>
          )
        },
        size: 180,
      },
      {
        accessorKey: "location",
        header: ({ column }) => (
          <DataGridColumnHeader title="Location" column={column} />
        ),
        cell: ({ row }) => {
          const item = row.original

          if (!item.location || !item.flag) {
            return <span className="text-muted-foreground">-</span>
          }

          return (
            <div className="flex items-center gap-1.5">
              <img
                src={`https://flagcdn.com/${item.flag.toLowerCase()}.svg`}
                alt={item.flag}
                className="size-4 rounded-full object-cover"
              />
              <div className="text-foreground font-medium">{item.location}</div>
            </div>
          )
        },
        size: 180,
      },
      {
        accessorKey: "status",
        id: "status",
        header: ({ column }) => (
          <DataGridColumnHeader title="Status" column={column} />
        ),
        cell: ({ row }) => {
          if (row.original.status === "active") {
            return <Badge variant="success-outline">Active</Badge>
          }

          return <Badge variant="warning-outline">Inactive</Badge>
        },
        size: 130,
      },
    ],
    []
  )

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: demoData,
    pageCount: Math.ceil((demoData?.length || 0) / pagination.pageSize),
    getRowId: (row: IData) => row.id,
    getSubRows: (row) => row.children,
    state: {
      pagination,
      sorting,
      expanded,
    },
    // Keep expanded children on the same page as their parent.
    paginateExpandedRows: false,
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
    onExpandedChange: setExpanded,
  })

  return (
    <DataGrid
      table={table}
      recordCount={demoData?.length || 0}
      tableLayout={{
        columnsResizable: true,
        columnsMovable: true,
        columnsVisibility: true,
      }}
    >
      <div className="w-full space-y-2.5">
        <Card className="overflow-hidden p-0">
          <DataGridContainer>
            <DataGridScrollArea>
              <DataGridTable />
            </DataGridScrollArea>
          </DataGridContainer>
        </Card>
        <DataGridPagination sizes={[4, 8, 16]} />
      </div>
    </DataGrid>
  )
}