{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-01",
  "title": "Supabase Data Table",
  "description": "Paginated data table with global search and sortable columns. Two modes: pass a data array for client-side rendering, or a Supabase table name for server-side pagination. Supports text, date, badge, tags, and status cell types.",
  "dependencies": [
    "lucide-react",
    "@tanstack/react-table"
  ],
  "registryDependencies": [
    "table",
    "badge",
    "button",
    "input",
    "dropdown-menu"
  ],
  "files": [
    {
      "path": "src/registry/blocks/data-table-01/data-table.tsx",
      "content": "import { DataTableClient } from \"./components/client\";\nimport { getData } from \"./data-table-actions\";\nimport { DEMO_DATA } from \"./data/demo\";\nimport type { ColumnDef, CellType } from \"./lib/utils\";\n\nconst SEARCHABLE_TYPES = new Set<CellType>([\"text\", \"badge\", \"status\"]);\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Row type — adapt fields to your own data model\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type Row = {\n  id: string;\n  name: string;\n  email: string;\n  status: \"active\" | \"inactive\" | \"pending\";\n  category: string;\n  tags?: string[];\n  created_at: string;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Column definitions — customize to your data model\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const COLUMNS: ColumnDef<Row>[] = [\n  { key: \"name\", header: \"Name\", type: \"text\", sortable: true },\n  { key: \"email\", header: \"Email\", type: \"text\" },\n  {\n    key: \"status\",\n    header: \"Status\",\n    type: \"status\",\n    statusConfig: {\n      active: { label: \"Active\", dot: \"bg-green-500\" },\n      inactive: { label: \"Inactive\", dot: \"bg-muted-foreground\" },\n      pending: { label: \"Pending\", dot: \"bg-amber-500\" },\n    },\n  },\n  { key: \"category\", header: \"Category\", type: \"badge\" },\n  { key: \"tags\", header: \"Tags\", type: \"tags\" },\n  {\n    key: \"created_at\",\n    header: \"Date\",\n    type: \"date\",\n    align: \"right\",\n    sortable: true,\n  },\n];\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport const PAGE_SIZE = 8;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Server Component\n//\n// Option 1 — static data (client-side pagination/sort/search):\n//   <DataTable01 data={rows} columns={COLUMNS} />\n//\n// Option 2 — Supabase table (server-side pagination/sort/search):\n//   <DataTable01 tableName=\"my_table\" columns={COLUMNS} />\n// ─────────────────────────────────────────────────────────────────────────────\n\ninterface DataTable01Props {\n  data?: Row[];\n  tableName?: string;\n  columns?: ColumnDef<Row>[];\n  searchColumns?: string[];\n}\n\nexport default async function DataTable01({\n  data,\n  tableName,\n  columns = COLUMNS,\n  searchColumns,\n}: DataTable01Props) {\n  // Option 2: Supabase mode — fetch first page server-side\n  if (tableName) {\n    const resolvedSearchColumns =\n      searchColumns ?? columns.filter((c) => SEARCHABLE_TYPES.has(c.type)).map((c) => c.key);\n\n    const { data: initialData, total } = await getData({\n      page: 1,\n      pageSize: PAGE_SIZE,\n      tableName,\n      searchColumns: resolvedSearchColumns,\n    });\n\n    return (\n      <DataTableClient\n        initialData={initialData}\n        initialTotal={total}\n        tableName={tableName}\n        fetchAction={getData}\n        columns={columns}\n        searchColumns={resolvedSearchColumns}\n      />\n    );\n  }\n\n  // Option 1: static data mode (defaults to DEMO_DATA for preview)\n  return <DataTableClient data={data ?? DEMO_DATA} columns={columns} />;\n}\n",
      "type": "registry:component",
      "target": "components/data-table-01/data-table.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/data-table-actions.ts",
      "content": "\"use server\";\n\nimport { createClient } from \"@/lib/supabase/server\";\nimport type { FetchParams, FetchResult } from \"./lib/utils\";\nimport { PAGE_SIZE, type Row } from \"./data-table\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport async function getData({\n  page,\n  pageSize = PAGE_SIZE,\n  tableName,\n  sort,\n  globalSearch,\n  searchColumns,\n}: FetchParams): Promise<FetchResult<Row>> {\n  const supabase = await createClient();\n  if (!supabase) throw new Error(\"Supabase is not configured.\");\n\n  const term = globalSearch?.trim();\n  const orFilter =\n    term && searchColumns?.length\n      ? searchColumns.map((col) => `${col}.ilike.%${term}%`).join(\",\")\n      : null;\n\n  // 1. Count query — HEAD request, no rows returned, always reflects the full total\n  const baseCount = supabase.from(tableName).select(\"*\", { count: \"exact\", head: true });\n  const { count, error: countError } = await (orFilter ? baseCount.or(orFilter) : baseCount);\n  if (countError) throw new Error(countError.message);\n\n  // 2. Data query — paginated rows only\n  const s = sort?.[0];\n  const from = (page - 1) * pageSize;\n  const baseData = supabase.from(tableName).select(\"*\");\n  const { data, error: dataError } = await (orFilter ? baseData.or(orFilter) : baseData)\n    .order(s?.id ?? \"created_at\", { ascending: !(s?.desc ?? true) })\n    .range(from, from + pageSize - 1);\n  if (dataError) throw new Error(dataError.message);\n\n  return {\n    data: (data ?? []) as Row[],\n    total: count ?? 0,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/data-table-01/data-table-actions.ts"
    },
    {
      "path": "src/registry/blocks/data-table-01/lib/utils.ts",
      "content": "// ─────────────────────────────────────────────────────────────────────────────\n// Cell types supported by the table\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type CellType = \"text\" | \"date\" | \"badge\" | \"tags\" | \"status\";\n\nexport type StatusVariant = {\n  label: string;\n  dot: string; // bg-* Tailwind class for the colored dot\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Column definition API\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ColumnDef<\n  T extends Record<string, unknown> = Record<string, unknown>,\n> {\n  key: keyof T & string;\n  header: string;\n  type: CellType;\n  align?: \"left\" | \"right\";\n  sortable?: boolean;\n  statusConfig?: Record<string, StatusVariant>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Fetch API (used by server action and client)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type SortParam = {\n  id: string;\n  desc: boolean;\n};\n\nexport type FetchParams = {\n  page: number;\n  pageSize: number;\n  tableName: string;\n  sort?: SortParam[];\n  globalSearch?: string;\n  searchColumns?: string[];\n};\n\nexport type FetchResult<T> = {\n  data: T[];\n  total: number;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Date formatter — import in cell-renderers.tsx for custom cell types\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function formatDate(iso: string) {\n  return new Date(iso).toLocaleDateString(\"en-US\", {\n    month: \"short\",\n    day: \"numeric\",\n    year: \"numeric\",\n  });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Pagination page list builder\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function buildPageList(total: number, current: number): (number | \"…\")[] {\n  if (total <= 1) return total === 1 ? [1] : [];\n\n  const show = new Set<number>();\n  show.add(1);\n  show.add(total);\n  show.add(current);\n\n  // Peek: on page 1 show page 2; on last page show second-to-last\n  if (current === 1 && total > 2) show.add(2);\n  if (current === total && total > 2) show.add(total - 1);\n\n  // Left neighbor: only if it leaves a gap after page 1 (avoids 3 consecutive pages from the start)\n  if (current - 1 > 2) show.add(current - 1);\n  // Right neighbor: only when current is past the \"prefix zone\" of pages 1-2\n  if (current >= 3 && current + 1 < total) show.add(current + 1);\n\n  const sorted = Array.from(show).sort((a, b) => a - b);\n  const pages: (number | \"…\")[] = [];\n  for (let i = 0; i < sorted.length; i++) {\n    if (i > 0 && sorted[i] - sorted[i - 1] > 1) pages.push(\"…\");\n    pages.push(sorted[i]);\n  }\n  return pages;\n}\n",
      "type": "registry:component",
      "target": "components/data-table-01/lib/utils.ts"
    },
    {
      "path": "src/registry/blocks/data-table-01/components/client.tsx",
      "content": "\"use client\";\n\nimport { useState, useTransition, useCallback, useRef, useEffect, useMemo } from \"react\";\nimport {\n  useReactTable,\n  getCoreRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  type SortingState,\n  type PaginationState,\n} from \"@tanstack/react-table\";\nimport type { Row } from \"../data-table\";\nimport type { FetchParams, FetchResult, ColumnDef } from \"../lib/utils\";\nimport type { ColumnDef as TanStackColumnDef } from \"@tanstack/react-table\";\nimport { renderCell } from \"./cell-renderers\";\nimport { SearchBar } from \"./searchbar\";\nimport { Table } from \"./table\";\nimport { Pagination } from \"./pagination\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst PAGE_SIZE = 8;\n\nexport interface DataTableClientProps {\n  columns: ColumnDef<Row>[];\n  /** Static mode: all rows provided — pagination/sort/search handled client-side */\n  data?: Row[];\n  /** Server mode: first-page rows fetched server-side */\n  initialData?: Row[];\n  initialTotal?: number;\n  tableName?: string;\n  fetchAction?: (params: FetchParams) => Promise<FetchResult<Row>>;\n  searchColumns?: string[];\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function DataTableClient(props: DataTableClientProps) {\n  const isStatic = props.data !== undefined;\n\n  const [serverData, setServerData] = useState<Row[]>(props.initialData ?? []);\n  const [total, setTotal] = useState(props.initialTotal ?? 0);\n  const [sorting, setSorting] = useState<SortingState>([]);\n  const [pagination, setPagination] = useState<PaginationState>({\n    pageIndex: 0,\n    pageSize: PAGE_SIZE,\n  });\n  const [globalSearch, setGlobalSearch] = useState(\"\");\n  const [isPending, startTransition] = useTransition();\n  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const isMounted = useRef(false);\n\n  const tanStackColumns: TanStackColumnDef<Row>[] = props.columns.map((col) => ({\n    id: col.key,\n    accessorKey: col.key,\n    header: col.header,\n    enableSorting: col.sortable ?? false,\n    cell: ({ row }) => renderCell(row.original, col),\n  }));\n\n  // Static mode: pre-filter before handing to TanStack\n  const staticData = useMemo(() => {\n    if (!isStatic) return [];\n    const all = props.data ?? [];\n    const q = globalSearch.trim().toLowerCase();\n    if (!q) return all;\n    return all.filter((r) =>\n      Object.values(r).some((v) => String(v ?? \"\").toLowerCase().includes(q)),\n    );\n  }, [isStatic, props.data, globalSearch]);\n\n  const table = useReactTable(\n    isStatic\n      ? {\n          data: staticData ?? [],\n          columns: tanStackColumns,\n          state: { sorting, pagination },\n          onSortingChange: setSorting,\n          onPaginationChange: setPagination,\n          getCoreRowModel: getCoreRowModel(),\n          getPaginationRowModel: getPaginationRowModel(),\n          getSortedRowModel: getSortedRowModel(),\n        }\n      : {\n          data: serverData,\n          columns: tanStackColumns,\n          pageCount: Math.max(1, Math.ceil(total / pagination.pageSize)),\n          state: { sorting, pagination },\n          onSortingChange: setSorting,\n          onPaginationChange: setPagination,\n          manualPagination: true,\n          manualSorting: true,\n          getCoreRowModel: getCoreRowModel(),\n        },\n  );\n\n  // ── Server mode: helpers ──────────────────────────────────────────────────\n\n  const doFetch = useCallback(\n    (params: FetchParams) => {\n      startTransition(async () => {\n        const result = await props.fetchAction!(params);\n        setServerData(result.data);\n        setTotal(result.total);\n      });\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [props.fetchAction],\n  );\n\n  const buildParams = useCallback(\n    (page: number, ps: number, sort: SortingState, search: string): FetchParams => ({\n      page,\n      pageSize: ps,\n      tableName: props.tableName!,\n      sort: sort.map((s) => ({ id: s.id, desc: s.desc })),\n      globalSearch: search.trim() || undefined,\n      searchColumns: props.searchColumns,\n    }),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [props.tableName, props.searchColumns],\n  );\n\n  // Server mode: immediate fetch on sort / pagination change\n  useEffect(() => {\n    if (isStatic) return;\n    if (!isMounted.current) {\n      isMounted.current = true;\n      return;\n    }\n    doFetch(buildParams(pagination.pageIndex + 1, pagination.pageSize, sorting, globalSearch));\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [sorting, pagination]);\n\n  // Server mode: debounced fetch on search change → reset to page 1\n  useEffect(() => {\n    if (isStatic) return;\n    if (debounceRef.current) clearTimeout(debounceRef.current);\n    debounceRef.current = setTimeout(() => {\n      doFetch(buildParams(1, pagination.pageSize, sorting, globalSearch));\n      setPagination((p) => ({ ...p, pageIndex: 0 }));\n    }, 300);\n    return () => {\n      if (debounceRef.current) clearTimeout(debounceRef.current);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [globalSearch]);\n\n  // Static mode: reset to page 1 when search changes\n  useEffect(() => {\n    if (!isStatic) return;\n    setPagination((p) => ({ ...p, pageIndex: 0 }));\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [globalSearch]);\n\n  // ─────────────────────────────────────────────────────────────────────────\n\n  const displayTotal = isStatic ? (staticData ?? []).length : total;\n\n  return (\n    <div className=\"w-full\">\n      <SearchBar\n        globalSearch={globalSearch}\n        setGlobalSearch={setGlobalSearch}\n        isPending={isPending}\n      />\n      <Table\n        table={table}\n        columns={props.columns}\n        tanStackColumns={tanStackColumns}\n        isPending={isPending}\n      />\n      <Pagination\n        table={table}\n        total={displayTotal}\n        pageIndex={pagination.pageIndex}\n        pageSize={pagination.pageSize}\n        isPending={isPending}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/data-table-01/components/client.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/components/cell-renderers.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\nimport type { Row } from \"../data-table\";\nimport { formatDate, type ColumnDef } from \"../lib/utils\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Cell renderer — add your own cell types below (e.g. image, currency, boolean)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport function renderCell(row: Row, col: ColumnDef<Row>): React.ReactNode {\n  const raw = row[col.key as keyof Row];\n\n  switch (col.type) {\n    case \"status\": {\n      const key = String(raw);\n      const variant = col.statusConfig?.[key];\n      if (!variant)\n        return <span className=\"text-sm text-muted-foreground\">{key}</span>;\n      return (\n        <span className=\"inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium border border-border text-foreground\">\n          <span className={cn(\"size-1.5 rounded-full shrink-0\", variant.dot)} />\n          {variant.label}\n        </span>\n      );\n    }\n\n    case \"date\":\n      return (\n        <span className=\"text-sm text-muted-foreground\">\n          {formatDate(String(raw))}\n        </span>\n      );\n\n    case \"badge\":\n      return <Badge variant=\"outline\">{String(raw ?? \"\")}</Badge>;\n\n    case \"tags\": {\n      const tagList = Array.isArray(raw) ? (raw as string[]) : [];\n      if (tagList.length === 0) return null;\n      return (\n        <div className=\"flex flex-wrap gap-1\">\n          {tagList.map((tag) => (\n            <Badge key={tag} variant=\"secondary\" className=\"text-xs\">{tag}</Badge>\n          ))}\n        </div>\n      );\n    }\n\n    case \"text\":\n    default:\n      return (\n        <span className=\"text-sm text-foreground\">{String(raw ?? \"\")}</span>\n      );\n  }\n}\n",
      "type": "registry:component",
      "target": "components/data-table-01/components/cell-renderers.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/components/table.tsx",
      "content": "\"use client\";\n\nimport type { Table as TanStackTable, ColumnDef as TanStackColumnDef } from \"@tanstack/react-table\";\nimport { flexRender } from \"@tanstack/react-table\";\nimport { ArrowUp, ArrowDown, ArrowUpDown } from \"lucide-react\";\nimport {\n  Table as ShadcnTable,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\nimport type { Row } from \"../data-table\";\nimport type { ColumnDef } from \"../lib/utils\";\n\ninterface TableProps {\n  table: TanStackTable<Row>;\n  columns: ColumnDef<Row>[];\n  tanStackColumns: TanStackColumnDef<Row>[];\n  isPending: boolean;\n}\n\nexport function Table({\n  table,\n  columns,\n  tanStackColumns,\n  isPending,\n}: TableProps) {\n  return (\n    <ShadcnTable>\n      <TableHeader>\n        {table.getHeaderGroups().map((hg) => (\n          <TableRow key={hg.id} className=\"hover:bg-transparent border-b\">\n            {hg.headers.map((header, i) => {\n              const col = columns.find((c) => c.key === header.id);\n              const canSort = header.column.getCanSort();\n              const sorted = header.column.getIsSorted();\n\n              return (\n                <TableHead\n                  key={header.id}\n                  className={cn(\n                    \"h-11 text-sm font-normal text-muted-foreground select-none\",\n                    i === 0 ? \"pl-6\" : \"pl-4\",\n                    i === hg.headers.length - 1 && \"pr-6\",\n                    col?.align === \"right\" && \"text-right\",\n                    canSort && \"cursor-pointer\",\n                  )}\n                  onClick={canSort ? header.column.getToggleSortingHandler() : undefined}\n                >\n                  <div className={cn(\"flex items-center gap-1\", col?.align === \"right\" && \"justify-end\")}>\n                    {flexRender(header.column.columnDef.header, header.getContext())}\n                    {canSort && (\n                      <span className=\"text-muted-foreground/50\">\n                        {sorted === \"asc\" ? (\n                          <ArrowUp className=\"size-3.5\" />\n                        ) : sorted === \"desc\" ? (\n                          <ArrowDown className=\"size-3.5\" />\n                        ) : (\n                          <ArrowUpDown className=\"size-3.5\" />\n                        )}\n                      </span>\n                    )}\n                  </div>\n                </TableHead>\n              );\n            })}\n          </TableRow>\n        ))}\n      </TableHeader>\n\n      <TableBody className={cn(\"transition-opacity duration-150\", isPending && \"opacity-40 pointer-events-none\")}>\n        {table.getRowModel().rows.length === 0 ? (\n          <TableRow>\n            <TableCell\n              colSpan={tanStackColumns.length}\n              className=\"py-20 text-center text-sm text-muted-foreground\"\n            >\n              No results found.\n            </TableCell>\n          </TableRow>\n        ) : (\n          table.getRowModel().rows.map((row) => (\n            <TableRow key={row.id} className=\"border-b last:border-0 hover:bg-muted/40\">\n              {row.getVisibleCells().map((cell, i) => {\n                const col = columns.find((c) => c.key === cell.column.id);\n                return (\n                  <TableCell\n                    key={cell.id}\n                    className={cn(\n                      \"py-4\",\n                      i === 0 ? \"pl-6\" : \"pl-4\",\n                      i === row.getVisibleCells().length - 1 && \"pr-6\",\n                      col?.align === \"right\" && \"text-right\",\n                    )}\n                  >\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </TableCell>\n                );\n              })}\n            </TableRow>\n          ))\n        )}\n      </TableBody>\n    </ShadcnTable>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/data-table-01/components/table.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/components/searchbar.tsx",
      "content": "\"use client\";\n\nimport type { Dispatch, SetStateAction } from \"react\";\nimport { Search, Loader2 } from \"lucide-react\";\nimport { Input } from \"@/components/ui/input\";\n\ninterface SearchBarProps {\n  globalSearch: string;\n  setGlobalSearch: Dispatch<SetStateAction<string>>;\n  isPending: boolean;\n}\n\nexport function SearchBar({\n  globalSearch,\n  setGlobalSearch,\n  isPending,\n}: SearchBarProps) {\n  return (\n    <div className=\"flex items-center border-b px-4 py-3\">\n      <div className=\"relative\">\n        <Search className=\"absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground pointer-events-none\" />\n        <Input\n          placeholder=\"Search…\"\n          value={globalSearch}\n          onChange={(e) => setGlobalSearch(e.target.value)}\n          className=\"h-9 w-56 pl-8 pr-7 text-sm\"\n        />\n        {isPending && (\n          <Loader2 className=\"absolute right-2.5 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-muted-foreground\" />\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/data-table-01/components/searchbar.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/components/pagination.tsx",
      "content": "import type { Table } from \"@tanstack/react-table\";\nimport { ChevronLeft, ChevronRight, SlidersHorizontal } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\nimport type { Row as Transaction } from \"../data-table\";\nimport { buildPageList } from \"../lib/utils\";\n\nconst PAGE_SIZE_OPTIONS = [8, 16, 32];\n\ninterface PaginationProps {\n  table: Table<Transaction>;\n  total: number;\n  pageIndex: number;\n  pageSize: number;\n  isPending: boolean;\n}\n\nexport function Pagination({\n  table,\n  total,\n  pageIndex,\n  pageSize,\n  isPending,\n}: PaginationProps) {\n  const start = pageIndex * pageSize + 1;\n  const end = Math.min((pageIndex + 1) * pageSize, total);\n\n  return (\n    <div className=\"flex items-center justify-between border-t px-6 py-3\">\n      {/* Left: count + rows-per-page */}\n      <div className=\"flex items-center gap-3\">\n        <p className=\"text-sm text-muted-foreground\">\n          {total === 0 ? \"No results\" : `${start}–${end} of ${total}`}\n        </p>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button\n              variant=\"ghost\"\n              size=\"sm\"\n              className=\"h-7 gap-1 text-xs text-muted-foreground px-2\"\n            >\n              {pageSize} / page\n              <SlidersHorizontal className=\"size-3\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"start\" className=\"w-32\">\n            {PAGE_SIZE_OPTIONS.map((size) => (\n              <DropdownMenuItem\n                key={size}\n                className={cn(\"text-sm\", size === pageSize && \"font-medium\")}\n                onClick={() => {\n                  table.setPageSize(size);\n                  table.setPageIndex(0);\n                }}\n              >\n                {size} rows\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      {/* Right: page buttons */}\n      <div className=\"flex items-center gap-0.5\">\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"h-8 w-8 text-muted-foreground\"\n          onClick={() => table.previousPage()}\n          disabled={!table.getCanPreviousPage() || isPending}\n        >\n          <ChevronLeft className=\"size-4\" />\n        </Button>\n        {buildPageList(table.getPageCount(), pageIndex + 1).map((item, i) =>\n          item === \"…\" ? (\n            <span\n              key={`gap-${i}`}\n              className=\"px-1 text-sm text-muted-foreground select-none\"\n            >\n              …\n            </span>\n          ) : (\n            <Button\n              key={item}\n              variant=\"ghost\"\n              size=\"sm\"\n              className={cn(\n                \"h-8 min-w-8 px-2 text-sm\",\n                item === pageIndex + 1\n                  ? \"text-foreground font-medium bg-muted hover:bg-muted\"\n                  : \"text-muted-foreground font-normal\",\n              )}\n              onClick={() => table.setPageIndex(item - 1)}\n              disabled={isPending}\n            >\n              {item}\n            </Button>\n          ),\n        )}\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          className=\"h-8 w-8 text-muted-foreground\"\n          onClick={() => table.nextPage()}\n          disabled={!table.getCanNextPage() || isPending}\n        >\n          <ChevronRight className=\"size-4\" />\n        </Button>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/data-table-01/components/pagination.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/data/demo.ts",
      "content": "import type { Row } from \"../data-table\";\n\nexport const DEMO_DATA: Row[] = [\n  { id: \"1\",  name: \"Peach Princess\",   email: \"peach@example.com\",      status: \"active\",   category: \"SaaS\",       tags: [\"API\", \"Growth\"],  created_at: \"2026-02-19T19:42:27Z\" },\n  { id: \"2\",  name: \"Carmella Rossi\",   email: \"carmella@example.com\",   status: \"inactive\", category: \"E-commerce\", tags: [\"Refund\"],          created_at: \"2026-02-19T18:10:00Z\" },\n  { id: \"3\",  name: \"Luigi Verde\",      email: \"luigi@example.com\",      status: \"pending\",  category: \"Services\",   tags: [\"B2B\", \"Annual\"],   created_at: \"2026-02-19T17:05:00Z\" },\n  { id: \"4\",  name: \"Toad Mushroom\",    email: \"toad@example.com\",       status: \"active\",   category: \"SaaS\",       tags: [\"Enterprise\"],      created_at: \"2026-02-18T14:30:00Z\" },\n  { id: \"5\",  name: \"Monserrat Diaz\",   email: \"monserrat@example.com\",  status: \"pending\",  category: \"Finance\",    tags: [\"Finance\"],         created_at: \"2026-02-18T11:20:00Z\" },\n  { id: \"6\",  name: \"Mario Rossi\",      email: \"mario@example.com\",      status: \"active\",   category: \"E-commerce\", tags: [\"Growth\"],          created_at: \"2026-02-17T09:00:00Z\" },\n  { id: \"7\",  name: \"Bowser King\",      email: \"bowser@example.com\",     status: \"inactive\", category: \"Services\",   tags: [\"Disputed\"],        created_at: \"2026-02-17T08:45:00Z\" },\n  { id: \"8\",  name: \"Daisy Fiore\",      email: \"daisy@example.com\",      status: \"active\",   category: \"SaaS\",       tags: [\"SaaS\", \"Growth\"],  created_at: \"2026-02-16T16:30:00Z\" },\n  { id: \"9\",  name: \"Yoshi Green\",      email: \"yoshi@example.com\",      status: \"pending\",  category: \"Finance\",    tags: [],                  created_at: \"2026-02-16T13:00:00Z\" },\n  { id: \"10\", name: \"Silas Blanc\",      email: \"silas@example.com\",      status: \"active\",   category: \"E-commerce\", tags: [],                  created_at: \"2026-02-15T11:15:00Z\" },\n  { id: \"11\", name: \"Abe Smith\",        email: \"abe@example.com\",        status: \"active\",   category: \"Services\",   tags: [],                  created_at: \"2026-02-15T09:30:00Z\" },\n  { id: \"12\", name: \"Ken Dupont\",       email: \"ken@example.com\",        status: \"active\",   category: \"SaaS\",       tags: [],                  created_at: \"2026-02-14T14:00:00Z\" },\n  { id: \"13\", name: \"Wario Black\",      email: \"wario@example.com\",      status: \"inactive\", category: \"Finance\",    tags: [],                  created_at: \"2026-02-14T10:45:00Z\" },\n  { id: \"14\", name: \"Rosalina Star\",    email: \"rosalina@example.com\",   status: \"pending\",  category: \"E-commerce\", tags: [],                  created_at: \"2026-02-13T16:20:00Z\" },\n  { id: \"15\", name: \"Birdo Pink\",       email: \"birdo@example.com\",      status: \"active\",   category: \"Services\",   tags: [],                  created_at: \"2026-02-13T08:00:00Z\" },\n  { id: \"16\", name: \"Koopa Shell\",      email: \"koopa@example.com\",      status: \"active\",   category: \"SaaS\",       tags: [],                  created_at: \"2026-02-12T13:30:00Z\" },\n];\n",
      "type": "registry:component",
      "target": "components/data-table-01/data/demo.ts"
    }
  ],
  "meta": {
    "isPro": true,
    "badges": [
      "supabase"
    ],
    "image": "/r/previews/data-table-01.webp",
    "imageDark": "/r/previews/data-table-01-dark.webp",
    "prose": {
      "about": "Data Table 01 is a paginated, sortable, and searchable data table with two rendering modes: client-side (pass a data array) and server-side (pass a Supabase table name for automatic server-side pagination, sorting, and search). Built with TanStack Table and shadcn/ui Table components, it supports text, date, badge, tags, and status cell types configurable per column. In server-side mode, a Next.js Server Action handles all Supabase queries — no API route needed.",
      "whenToUse": [
        "Admin panels and dashboards that need to display and navigate large datasets from a Supabase database",
        "SaaS back-offices where sorting and searching table data are core user workflows",
        "Internal tools where a production-ready data table saves days of TanStack Table configuration",
        "Any Next.js 15 project that needs server-side pagination without writing a custom data-fetching layer"
      ],
      "notes": "In server-side mode, pass your Supabase table name and column definitions — the Server Action handles count queries, range pagination, and search filters automatically. For client-side mode, pass the full dataset as a prop and pagination happens in the browser.",
      "faqs": [
        {
          "question": "Do I need Supabase to use this block?",
          "answer": "No, it supports a client-side mode where you pass a plain data array. Supabase is only required for the server-side pagination mode."
        },
        {
          "question": "How does server-side pagination work?",
          "answer": "Pass your Supabase table name and column definitions — a Next.js Server Action handles count queries, range pagination, and search filters automatically, no API route needed."
        },
        {
          "question": "What column types does it support?",
          "answer": "Text, date, badge, tags, and status cell types, configurable per column."
        }
      ]
    }
  },
  "categories": [
    "dashboard"
  ],
  "type": "registry:block"
}