{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table",
  "title": "Table",
  "author": "AlgorizeTech",
  "description": "Composable table primitives — you write the rows, so no column config and no required data shape. Use it for tabular data you want full control over.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://avaraui.com/r/toggle-group.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/TableBlock.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  ArrowDown,\n  ArrowUp,\n  Check,\n  ChevronDown,\n  ChevronLeft,\n  ChevronRight,\n  ChevronsUpDown,\n  ChevronUp,\n  Download,\n  Minus,\n  RotateCcw,\n  Search,\n  X,\n} from \"lucide-react\";\nimport React, {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { accentStyle } from \"@/lib/accent\";\nimport { type Accessor, DataTableContext, type DataTableContextValue, type SortType, TableContext, type TableSize, type TableVariant, getAccentColor, getThemeStyles, resolveAccessor, useDataTable, useTableStyles } from \"./table-core\";\nimport { ToggleGroup, ToggleGroupItem } from \"./ToggleGroupBlock\";\n\n// ==========================================\n// DYNAMIC THEME ENGINE\n// ==========================================\nexport type TableColor = \"indigo\" | \"rose\" | \"emerald\" | \"zinc\" | (string & {});\nexport type TableAlign = \"left\" | \"center\" | \"right\";\n\n// ==========================================\n// LOOK-UP TABLES\n// ==========================================\nconst sizeClasses: Record<TableSize, { th: string; td: string; bar: string }> = {\n  sm: {\n    th: \"px-3 py-2 text-[11px]\",\n    td: \"px-3 py-2 text-xs\",\n    bar: \"px-3 py-2.5\",\n  },\n  md: {\n    th: \"px-4 lg:px-6 py-3.5 text-xs\",\n    td: \"px-4 lg:px-6 py-3.5 text-sm\",\n    bar: \"px-4 lg:px-6 py-4\",\n  },\n  lg: { th: \"px-6 py-4 text-xs\", td: \"px-6 py-4 text-base\", bar: \"px-6 py-5\" },\n};\n\n// `soft` rows breathe a little more and indent the outer columns on wide\n// screens, so the first and last cells line up with the toolbar above.\nconst softSizeClasses: Record<TableSize, { th: string; td: string; bar: string }> = {\n  sm: {\n    th: \"px-3 py-2 text-[10px]\",\n    td: \"px-3 py-2.5 text-xs\",\n    bar: \"px-3 py-2.5\",\n  },\n  md: {\n    th: \"px-4 py-3 text-[11px] lg:first:pl-6 lg:last:pr-6\",\n    td: \"px-4 py-3.5 text-sm lg:first:pl-6 lg:last:pr-6\",\n    bar: \"px-4 py-3.5 lg:px-6\",\n  },\n  lg: {\n    th: \"px-6 py-3.5 text-[11px]\",\n    td: \"px-6 py-4 text-base\",\n    bar: \"px-6 py-4\",\n  },\n};\n\nconst sizesFor = (variant: TableVariant) =>\n  variant === \"soft\" ? softSizeClasses : sizeClasses;\n\n/**\n * The `soft` variant's tints, mixed from the table colour on the wrapper so\n * every part reads them: the well (header band, search, filters), the row\n * hover and selected washes, and the tint an applied filter takes.\n */\nconst softTokens = (color?: string) =>\n  ({\n    \"--tbl-accent\": getAccentColor(color),\n    \"--tbl-well\":\n      \"color-mix(in oklab, var(--tbl-accent) 5%, color-mix(in oklab, var(--foreground) 5%, var(--card)))\",\n    \"--tbl-row-hover\": \"color-mix(in oklab, var(--tbl-accent) 4%, var(--card))\",\n    \"--tbl-row-selected\":\n      \"color-mix(in oklab, var(--tbl-accent) 8%, var(--card))\",\n    \"--tbl-tint\": \"color-mix(in oklab, var(--tbl-accent) 10%, var(--card))\",\n    \"--tbl-accent-ink\":\n      \"color-mix(in oklab, var(--tbl-accent) 78%, var(--foreground))\",\n  }) as React.CSSProperties;\n\n// Outside a `soft` table these fall back to the theme's muted surface, so\n// the shared parts (row actions, badges, empty states) work in every variant.\nconst WELL = \"bg-[var(--tbl-well,var(--muted))]\";\nconst FOCUS_RING =\n  \"outline-none focus-visible:ring-2 focus-visible:ring-[color-mix(in_oklab,var(--tbl-accent,var(--primary))_60%,transparent)]\";\n\ninterface VariantSkin {\n  wrapper: string;\n  heading: string;\n  footer: string;\n  headerRow: string;\n  row: string;\n  selectedRow: string;\n  expanded: string;\n}\n\nconst variantClasses: Record<TableVariant, VariantSkin> = {\n  default: {\n    wrapper: \"w-full bg-card text-card-foreground border border-primary/20 rounded-xl shadow-sm overflow-hidden\",\n    heading: \"border-b border-primary/15 bg-card\",\n    footer: \"border-t border-primary/15 bg-card\",\n    headerRow: \"bg-primary/[0.04] dark:bg-primary/[0.08] border-b border-primary/15\",\n    row: \"group border-b border-primary/10 last:border-0 transition-colors bg-card hover:bg-primary/[0.05] dark:hover:bg-primary/[0.08]\",\n    selectedRow: \"bg-primary/[0.08] dark:bg-primary/[0.12]\",\n    expanded: \"bg-primary/[0.03] dark:bg-primary/[0.06] border-b border-primary/10\",\n  },\n  minimal: {\n    wrapper: \"w-full bg-transparent\",\n    heading: \"border-b border-primary/15\",\n    footer: \"border-t border-primary/15\",\n    headerRow: \"border-b-2 border-primary/25\",\n    row: \"group border-b border-primary/10 last:border-0 transition-colors bg-transparent hover:bg-primary/[0.05] dark:hover:bg-primary/[0.08]\",\n    selectedRow: \"bg-primary/[0.08] dark:bg-primary/[0.12]\",\n    expanded: \"bg-primary/[0.03] dark:bg-primary/[0.06] border-b border-primary/10\",\n  },\n  glass: {\n    wrapper:\n      \"w-full bg-card/40 text-card-foreground backdrop-blur-xl border border-primary/20 rounded-xl shadow-xl overflow-hidden\",\n    heading: \"border-b border-primary/15\",\n    footer: \"border-t border-primary/15\",\n    headerRow:\n      \"bg-primary/[0.06] border-b border-primary/15\",\n    row: \"group border-b border-primary/10 last:border-0 transition-colors hover:bg-primary/[0.05]\",\n    selectedRow: \"bg-primary/[0.12]\",\n    expanded:\n      \"bg-primary/[0.05] border-b border-primary/10\",\n  },\n  // Separate borders: the dividers live on the cells, so a sticky cell paints\n  // its own. The wrapper does not clip, so filter and page-size menus can\n  // open past its edge; the scroll area rounds itself instead.\n  soft: {\n    wrapper:\n      \"relative w-full rounded-[20px] bg-card text-card-foreground ring-1 ring-foreground/[0.08] shadow-[0_1px_2px_rgb(16_24_40/0.04),0_16px_32px_-20px_rgb(16_24_40/0.14)] dark:shadow-none\",\n    heading: \"border-b border-foreground/[0.08]\",\n    footer: \"border-t border-foreground/[0.08]\",\n    // The band is painted on the row and on its cells, so the hairline gaps\n    // fractional column widths leave between cells show the same colour.\n    headerRow:\n      \"bg-[var(--tbl-well)] [&>*]:bg-[var(--tbl-well)] [&>*]:border-b [&>*]:border-foreground/[0.08]\",\n    // Dividers sit on top of each row (none on the first), so the last row\n    // never doubles the footer's line — even when hidden detail rows follow it.\n    row: \"group transition-colors duration-150 hover:bg-[var(--tbl-row-hover)] [&>*]:border-t [&>*]:border-foreground/[0.08] [&:first-child>*]:border-t-0\",\n    selectedRow: \"bg-[var(--tbl-row-selected)] hover:bg-[var(--tbl-row-selected)]\",\n    // An open detail row reads as part of the row above it: no divider between.\n    expanded: \"bg-[color-mix(in_oklab,var(--tbl-well)_60%,var(--card))]\",\n  },\n};\n\nconst alignClasses: Record<TableAlign, string> = {\n  left: \"text-left\",\n  center: \"text-center\",\n  right: \"text-right\",\n};\n\nconst alignFlex: Record<TableAlign, string> = {\n  left: \"justify-start\",\n  center: \"justify-center\",\n  right: \"justify-end\",\n};\n\n// ==========================================\n// PRIMITIVES\n// ==========================================\nexport interface TableProps extends React.HTMLAttributes<HTMLDivElement> {\n  variant?: TableVariant;\n  color?: TableColor;\n  size?: TableSize;\n}\n\nexport const Table = forwardRef<HTMLDivElement, TableProps>(\n  (\n    {\n      variant = \"default\",\n      color = \"primary\",\n      size = \"md\",\n      className,\n      children,\n      style: styleProp,\n      ...props\n    },\n    ref,\n  ) => {\n    const { theme, style } = getThemeStyles(color as string);\n    const accentCss = accentStyle(color as string);\n    return (\n      <TableContext.Provider value={{ variant, size, theme }}>\n        <div\n          ref={ref}\n          className={cn(\n            \"w-full\",\n            variantClasses[variant].wrapper,\n            variant === \"default\" && (theme.borderSoft || \"border-primary/25\"),\n            className\n          )}\n          style={{\n            ...accentCss,\n            ...style,\n            ...softTokens(color as string),\n            ...styleProp,\n          }}\n          {...props}\n        >\n          {children}\n        </div>\n      </TableContext.Provider>\n    );\n  },\n);\nTable.displayName = \"Table\";\n\n/** `title` is widened to ReactNode, so the DOM `title` attribute is omitted. */\nexport interface TableHeadingProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\"> {\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n}\n\nexport const TableHeading = forwardRef<HTMLDivElement, TableHeadingProps>(\n  ({ title, description, className, children, ...props }, ref) => {\n    const { variant, size, theme } = useTableStyles();\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex flex-wrap items-center justify-between gap-3\",\n          sizesFor(variant)[size].bar,\n          variantClasses[variant].heading,\n          (variant === \"default\" || variant === \"minimal\") && (theme.borderSoft || \"border-primary/12\"),\n          className,\n        )}\n        {...props}\n      >\n        {(title || description) && (\n          <div className=\"flex flex-col gap-0.5\">\n            {title && (\n              <h3\n                className={cn(\n                  \"text-base lg:text-lg text-foreground\",\n                  variant === \"soft\"\n                    ? \"font-medium tracking-[-0.01em]\"\n                    : \"font-semibold tracking-tight\",\n                )}\n              >\n                {title}\n              </h3>\n            )}\n            {description && (\n              <p className=\"text-sm text-muted-foreground\">{description}</p>\n            )}\n          </div>\n        )}\n        {children && (\n          <div className=\"flex flex-wrap items-center justify-end gap-2\">\n            {children}\n          </div>\n        )}\n      </div>\n    );\n  },\n);\nTableHeading.displayName = \"TableHeading\";\n\nexport const TableContent = forwardRef<\n  HTMLTableElement,\n  React.TableHTMLAttributes<HTMLTableElement>\n>(({ className, children, ...props }, ref) => {\n  const { variant } = useTableStyles();\n  const soft = variant === \"soft\";\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const [edge, setEdge] = useState(false);\n\n  // While more table hides past the right edge, a pinned (`sticky`) column\n  // casts a shadow over it.\n  useEffect(() => {\n    const element = scrollRef.current;\n    if (!soft || !element) return;\n    const update = () => {\n      const hidden =\n        element.scrollWidth - element.clientWidth - element.scrollLeft > 1;\n      setEdge((previous) => (previous === hidden ? previous : hidden));\n    };\n    update();\n    element.addEventListener(\"scroll\", update, { passive: true });\n    const observer = new ResizeObserver(update);\n    observer.observe(element);\n    if (element.firstElementChild) observer.observe(element.firstElementChild);\n    return () => {\n      element.removeEventListener(\"scroll\", update);\n      observer.disconnect();\n    };\n  }, [soft]);\n\n  if (!soft) {\n    return (\n      <div className=\"w-full overflow-x-auto\">\n        <table\n          ref={ref}\n          className={cn(\n            \"w-full caption-bottom border-collapse text-left\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </table>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      ref={scrollRef}\n      data-edge={edge || undefined}\n      className=\"group/scroll w-full [overflow-x:auto] [scrollbar-width:thin] [scrollbar-color:color-mix(in_oklab,var(--foreground)_12%,transparent)_transparent] first:rounded-t-[20px] last:rounded-b-[20px]\"\n    >\n      <table\n        ref={ref}\n        className={cn(\n          \"w-full caption-bottom border-separate border-spacing-0 text-left\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </table>\n    </div>\n  );\n});\nTableContent.displayName = \"TableContent\";\n\n// A pinned column keeps its own background (a row's tint cannot show through\n// a sticky cell) and follows the row's hover and selected washes.\nconst stickyHead: Record<TableVariant, string> = {\n  default: \"sticky right-0 z-[1] bg-muted\",\n  minimal: \"sticky right-0 z-[1] bg-background\",\n  glass: \"sticky right-0 z-[1] bg-card\",\n  soft: \"sticky right-0 z-[1] bg-[var(--tbl-well)] group-data-[edge=true]/scroll:shadow-[-10px_0_18px_-8px_rgb(15_23_42/0.2)] dark:group-data-[edge=true]/scroll:shadow-[-10px_0_18px_-8px_rgb(0_0_0/0.55)]\",\n};\nconst stickyCell: Record<TableVariant, string> = {\n  default:\n    \"sticky right-0 z-[1] bg-card group-hover:bg-[color-mix(in_oklab,var(--muted)_60%,var(--card))] group-data-[selected=true]:bg-muted\",\n  minimal: \"sticky right-0 z-[1] bg-background\",\n  glass: \"sticky right-0 z-[1] bg-card\",\n  soft: \"sticky right-0 z-[1] bg-card group-hover:bg-[var(--tbl-row-hover)] group-data-[selected=true]:bg-[var(--tbl-row-selected)] group-data-[edge=true]/scroll:shadow-[-10px_0_18px_-8px_rgb(15_23_42/0.2)] dark:group-data-[edge=true]/scroll:shadow-[-10px_0_18px_-8px_rgb(0_0_0/0.55)]\",\n};\n\nexport const TableHeader = forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, children, ...props }, ref) => (\n  <thead ref={ref} className={className} {...props}>\n    {children}\n  </thead>\n));\nTableHeader.displayName = \"TableHeader\";\n\nexport interface TableBodyProps\n  extends React.HTMLAttributes<HTMLTableSectionElement> {\n  /** Staggered entrance for direct child rows. */\n  animate?: boolean;\n  /** Change this to replay the entrance — e.g. on sort or page change. */\n  animateKey?: React.Key;\n}\n\nexport const TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(\n  ({ animate = false, animateKey, className, children, ...props }, ref) => {\n    const innerRef = useRef<HTMLTableSectionElement>(null);\n    React.useImperativeHandle(\n      ref,\n      () => innerRef.current as HTMLTableSectionElement,\n    );\n\n    useEffect(() => {\n      if (!animate || !innerRef.current) return;\n      const rows = innerRef.current.querySelectorAll<HTMLTableRowElement>(\n        \":scope > tr\",\n      );\n      if (!rows.length) return;\n      // `backwards` holds each row hidden through its stagger delay. The\n      // animation never writes the row's own style, and cancelling it (a new\n      // key mid-stagger, unmount) drops every row straight back to rest.\n      const animations = Array.from(rows, (row, index) =>\n        row.animate(\n          [\n            { opacity: 0, transform: \"translateY(15px)\" },\n            { opacity: 1, transform: \"translateY(0px)\" },\n          ],\n          {\n            duration: 400,\n            delay: index * 50,\n            // power2.out, exact\n            easing: \"cubic-bezier(0.3333, 1, 0.6667, 1)\",\n            fill: \"backwards\",\n          },\n        ),\n      );\n      return () => {\n        animations.forEach((animation) => animation.cancel());\n      };\n    }, [animate, animateKey]);\n\n    return (\n      <tbody ref={innerRef} className={className} {...props}>\n        {children}\n      </tbody>\n    );\n  },\n);\nTableBody.displayName = \"TableBody\";\n\nexport interface TableRowProps\n  extends React.HTMLAttributes<HTMLTableRowElement> {\n  isHeader?: boolean;\n  selected?: boolean;\n}\n\nexport const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(\n  (\n    { isHeader = false, selected = false, className, children, ...props },\n    ref,\n  ) => {\n    const { variant, theme } = useTableStyles();\n    const skin = variantClasses[variant];\n    return (\n      <tr\n        ref={ref}\n        data-selected={selected || undefined}\n        className={cn(\n          isHeader ? skin.headerRow : skin.row,\n          (variant === \"default\" || variant === \"minimal\") && (theme.borderSoft || \"border-primary/12\"),\n          !isHeader && selected && skin.selectedRow,\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </tr>\n    );\n  },\n);\nTableRow.displayName = \"TableRow\";\n\nexport interface TableHeadProps\n  extends Omit<React.ThHTMLAttributes<HTMLTableCellElement>, \"align\"> {\n  align?: TableAlign;\n  sortable?: boolean;\n  /** Controlled — you own the sort state. */\n  sortDirection?: \"asc\" | \"desc\" | null;\n  onSort?: () => void;\n  /** Pin the column to the right edge (pair it with `sticky` on its cells). */\n  sticky?: boolean;\n}\n\nexport const TableHead = forwardRef<HTMLTableCellElement, TableHeadProps>(\n  (\n    {\n      align = \"left\",\n      sortable = false,\n      sortDirection = null,\n      onSort,\n      sticky = false,\n      className,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const { variant, size } = useTableStyles();\n\n    if (variant === \"soft\") {\n      const SortIcon =\n        sortDirection === \"asc\"\n          ? ArrowUp\n          : sortDirection === \"desc\"\n            ? ArrowDown\n            : ChevronsUpDown;\n      return (\n        <th\n          ref={ref}\n          scope=\"col\"\n          aria-sort={\n            sortable\n              ? sortDirection === \"asc\"\n                ? \"ascending\"\n                : sortDirection === \"desc\"\n                  ? \"descending\"\n                  : \"none\"\n              : undefined\n          }\n          className={cn(\n            softSizeClasses[size].th,\n            \"whitespace-nowrap font-medium uppercase tracking-[0.06em] text-muted-foreground select-none\",\n            alignClasses[align],\n            sticky && stickyHead.soft,\n            className,\n          )}\n          {...props}\n        >\n          {sortable ? (\n            <button\n              type=\"button\"\n              onClick={onSort}\n              className={cn(\n                \"group/sort -mx-1.5 inline-flex items-center gap-1 rounded-md px-1.5 py-1 font-medium uppercase tracking-[0.06em] transition-colors hover:text-foreground\",\n                FOCUS_RING,\n                sortDirection && \"text-foreground\",\n                align === \"right\" && \"flex-row-reverse\",\n              )}\n            >\n              <span>{children}</span>\n              <SortIcon\n                aria-hidden=\"true\"\n                className={cn(\n                  \"size-3.5 shrink-0 transition-opacity\",\n                  sortDirection\n                    ? \"text-[var(--tbl-accent-ink)]\"\n                    : \"opacity-40 group-hover/sort:opacity-100\",\n                )}\n              />\n            </button>\n          ) : (\n            children\n          )}\n        </th>\n      );\n    }\n\n    return (\n      <th\n        ref={ref}\n        scope=\"col\"\n        aria-sort={\n          sortable\n            ? sortDirection === \"asc\"\n              ? \"ascending\"\n              : sortDirection === \"desc\"\n                ? \"descending\"\n                : \"none\"\n            : undefined\n        }\n        className={cn(\n          sizeClasses[size].th,\n          \"whitespace-nowrap font-semibold uppercase tracking-wider text-muted-foreground select-none\",\n          alignClasses[align],\n          sticky && stickyHead[variant],\n          className,\n        )}\n        {...props}\n      >\n        {sortable ? (\n          <button\n            type=\"button\"\n            onClick={onSort}\n            className={cn(\n              \"inline-flex items-center gap-1.5 font-semibold uppercase tracking-wider transition-colors hover:text-primary cursor-pointer\",\n              sortDirection && \"text-primary\",\n            )}\n          >\n            {children}\n            <span className=\"shrink-0\">\n              {sortDirection === \"asc\" ? (\n                <ChevronUp className=\"w-3.5 h-3.5 text-primary\" />\n              ) : sortDirection === \"desc\" ? (\n                <ChevronDown className=\"w-3.5 h-3.5 text-primary\" />\n              ) : (\n                <ChevronsUpDown className=\"w-3.5 h-3.5 opacity-50 group-hover:opacity-100\" />\n              )}\n            </span>\n          </button>\n        ) : (\n          children\n        )}\n      </th>\n    );\n  },\n);\nTableHead.displayName = \"TableHead\";\n\nexport interface TableCellProps\n  extends Omit<React.TdHTMLAttributes<HTMLTableCellElement>, \"align\"> {\n  align?: TableAlign;\n  /** Pin the cell to the right edge, e.g. a row-actions column. */\n  sticky?: boolean;\n}\n\nexport const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(\n  ({ align = \"left\", sticky = false, className, children, ...props }, ref) => {\n    const { variant, size } = useTableStyles();\n    return (\n      <td\n        ref={ref}\n        className={cn(\n          sizesFor(variant)[size].td,\n          \"align-middle\",\n          variant === \"soft\" ? \"text-foreground\" : \"text-foreground/80\",\n          alignClasses[align],\n          sticky && stickyCell[variant],\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </td>\n    );\n  },\n);\nTableCell.displayName = \"TableCell\";\n\nexport const TableFooter = forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, children, ...props }, ref) => {\n  const { variant, size, theme } = useTableStyles();\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        variant === \"soft\"\n          ? // Controls first on phones (where a thumb reaches), the count under them.\n            \"flex flex-col-reverse items-center justify-between gap-4 text-center md:flex-row md:text-left w-full\"\n          : \"flex flex-col sm:flex-row items-center justify-center sm:justify-between gap-3 text-center sm:text-left w-full\",\n        sizesFor(variant)[size].bar,\n        variantClasses[variant].footer,\n        (variant === \"default\" || variant === \"minimal\") && (theme.borderSoft || \"border-primary/12\"),\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n});\nTableFooter.displayName = \"TableFooter\";\n\n/** `title` is widened to ReactNode, so the DOM `title` attribute is omitted. */\nexport interface TableEmptyProps\n  extends Omit<React.TdHTMLAttributes<HTMLTableCellElement>, \"title\"> {\n  colSpan: number;\n  /** An icon shown in a round chip above the title. */\n  icon?: React.ReactNode;\n  /** What is missing — \"No users match “ana”\". Switches to the full empty state. */\n  title?: React.ReactNode;\n  /** What to do about it. */\n  description?: React.ReactNode;\n  /** A button under the copy, e.g. \"Clear search\". */\n  action?: React.ReactNode;\n}\n\nexport const TableEmpty = forwardRef<HTMLTableCellElement, TableEmptyProps>(\n  (\n    { colSpan, icon, title, description, action, className, children, ...props },\n    ref,\n  ) => {\n    const { variant } = useTableStyles();\n    // Plain message: the original one-line empty row.\n    if (!title && !icon) {\n      return (\n        <tr>\n          <td\n            ref={ref}\n            colSpan={colSpan}\n            className={cn(\n              \"py-12 text-center text-sm text-muted-foreground\",\n              className,\n            )}\n            {...props}\n          >\n            {children ?? \"No data available.\"}\n          </td>\n        </tr>\n      );\n    }\n    return (\n      <tr>\n        <td\n          ref={ref}\n          colSpan={colSpan}\n          className={cn(\"p-0\", variant === \"soft\" && \"border-0\", className)}\n          {...props}\n        >\n          <div\n            role=\"status\"\n            className=\"flex min-h-72 flex-col items-center justify-center px-6 py-14 text-center\"\n          >\n            {icon && (\n              <span\n                className={cn(\n                  \"grid size-12 place-items-center rounded-full text-muted-foreground [&_svg]:size-5\",\n                  WELL,\n                )}\n              >\n                {icon}\n              </span>\n            )}\n            {title && (\n              <p className=\"mt-4 text-[15px] font-medium text-foreground\">\n                {title}\n              </p>\n            )}\n            {description && (\n              <p className=\"mt-1 max-w-[44ch] text-pretty text-sm leading-6 text-muted-foreground\">\n                {description}\n              </p>\n            )}\n            {children}\n            {action && <div className=\"mt-5\">{action}</div>}\n          </div>\n        </td>\n      </tr>\n    );\n  },\n);\nTableEmpty.displayName = \"TableEmpty\";\n\nexport interface TableExpandedRowProps\n  extends React.HTMLAttributes<HTMLTableRowElement> {\n  open?: boolean;\n  colSpan: number;\n}\n\nexport const TableExpandedRow = forwardRef<\n  HTMLTableRowElement,\n  TableExpandedRowProps\n>(({ open = false, colSpan, className, children, ...props }, ref) => {\n  const { variant } = useTableStyles();\n  return (\n    <tr\n      ref={ref}\n      className={cn(\n        open ? variantClasses[variant].expanded : \"border-0\",\n        className,\n      )}\n      {...props}\n    >\n      <td colSpan={colSpan} className=\"p-0 border-0\">\n        <div\n          className={cn(\n            \"grid transition-all duration-300 ease-in-out\",\n            open ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\",\n          )}\n        >\n          <div className=\"overflow-hidden\">\n            <div className=\"p-6\">{children}</div>\n          </div>\n        </div>\n      </td>\n    </tr>\n  );\n});\nTableExpandedRow.displayName = \"TableExpandedRow\";\n\nexport interface TableExpandToggleProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  open?: boolean;\n}\n\nexport const TableExpandToggle = forwardRef<\n  HTMLButtonElement,\n  TableExpandToggleProps\n>(({ open = false, className, ...props }, ref) => {\n  const { variant } = useTableStyles();\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      aria-expanded={open}\n      aria-label={open ? \"Collapse row\" : \"Expand row\"}\n      className={cn(\n        variant === \"soft\"\n          ? cn(\n              \"grid size-8 place-items-center rounded-full text-muted-foreground transition-colors hover:bg-[var(--tbl-well)] hover:text-foreground cursor-pointer\",\n              FOCUS_RING,\n              open && \"bg-[var(--tbl-well)] text-foreground\",\n            )\n          : \"p-1 rounded-md text-muted-foreground hover:bg-primary/10 hover:text-primary transition-colors cursor-pointer\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronRight\n        aria-hidden=\"true\"\n        className={cn(\n          \"w-4 h-4 transition-transform duration-200 motion-reduce:transition-none\",\n          open && \"rotate-90\",\n        )}\n      />\n    </button>\n  );\n});\nTableExpandToggle.displayName = \"TableExpandToggle\";\n\nexport interface TableRowActionsProps\n  extends React.HTMLAttributes<HTMLDivElement> {\n  align?: TableAlign;\n  /**\n   * Keep actions visible instead of revealing them on row hover. Defaults to\n   * visible in the `soft` variant (where they are calm round icons) and to\n   * hover-reveal everywhere else.\n   */\n  alwaysVisible?: boolean;\n}\n\nexport const TableRowActions = forwardRef<HTMLDivElement, TableRowActionsProps>(\n  ({ align = \"right\", alwaysVisible, className, children, ...props }, ref) => {\n    const { variant } = useTableStyles();\n    const visible = alwaysVisible ?? variant === \"soft\";\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex items-center transition-opacity duration-200\",\n          variant === \"soft\" ? \"gap-0.5\" : \"gap-1\",\n          alignFlex[align],\n          !visible &&\n            \"opacity-0 group-hover:opacity-100 focus-within:opacity-100\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    );\n  },\n);\nTableRowActions.displayName = \"TableRowActions\";\n\nexport interface TableRowActionProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Required: the accessible name and the hover title of an icon-only button. */\n  label: string;\n  /** `danger` turns red on hover — keep it for destructive actions. */\n  tone?: \"default\" | \"danger\";\n}\n\n/** A round icon button for a row action (edit, delete…). Pass the icon as children. */\nexport const TableRowAction = forwardRef<HTMLButtonElement, TableRowActionProps>(\n  ({ label, tone = \"default\", type = \"button\", className, children, ...props }, ref) => {\n    const { variant } = useTableStyles();\n    return (\n      <button\n        ref={ref}\n        type={type}\n        aria-label={label}\n        title={label}\n        className={cn(\n          \"grid shrink-0 place-items-center text-muted-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-40 [&_svg]:size-4\",\n          FOCUS_RING,\n          variant === \"soft\" ? \"size-9 rounded-full\" : \"size-8 rounded-md\",\n          tone === \"danger\"\n            ? \"hover:bg-red-500/10 hover:text-red-600 dark:hover:text-red-400\"\n            : \"hover:bg-[var(--tbl-well,var(--muted))] hover:text-foreground\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </button>\n    );\n  },\n);\nTableRowAction.displayName = \"TableRowAction\";\n\nexport interface TableActionProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** `primary` for the panel's main action (\"Add user\"), `secondary` beside it, `ghost` for quiet ones. */\n  variant?: \"primary\" | \"secondary\" | \"ghost\";\n  size?: \"sm\" | \"md\";\n}\n\n/**\n * A toolbar button that matches the table — a pill in the `soft` variant.\n * Icons passed as children are sized for you.\n */\nexport const TableAction = forwardRef<HTMLButtonElement, TableActionProps>(\n  (\n    { variant = \"secondary\", size = \"md\", type = \"button\", className, children, ...props },\n    ref,\n  ) => {\n    const { variant: tableVariant, theme } = useTableStyles();\n    const soft = tableVariant === \"soft\";\n    return (\n      <button\n        ref={ref}\n        type={type}\n        className={cn(\n          \"inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap font-medium transition-[background-color,color,filter] duration-150 disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:shrink-0\",\n          FOCUS_RING,\n          soft\n            ? size === \"md\"\n              ? \"h-10 gap-2 rounded-full px-5 text-sm [&_svg]:size-4\"\n              : \"h-8 gap-1.5 rounded-full px-3.5 text-[13px] [&_svg]:size-3.5\"\n            : size === \"md\"\n              ? \"h-9 gap-1.5 rounded-lg px-3.5 text-[13px] [&_svg]:size-4\"\n              : \"h-8 gap-1.5 rounded-lg px-3 text-xs [&_svg]:size-3.5\",\n          variant === \"primary\" && cn(theme.bg, \"shadow-sm hover:brightness-110\"),\n          variant === \"secondary\" &&\n            \"bg-[var(--tbl-well,var(--muted))] text-foreground hover:bg-[color-mix(in_oklab,var(--foreground)_7%,var(--tbl-well,var(--muted)))]\",\n          variant === \"ghost\" &&\n            \"text-muted-foreground hover:bg-[var(--tbl-well,var(--muted))] hover:text-foreground\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </button>\n    );\n  },\n);\nTableAction.displayName = \"TableAction\";\n\nexport type TableBadgeTone = \"neutral\" | \"success\" | \"warning\" | \"danger\" | \"info\";\n\nconst badgeTones: Record<TableBadgeTone, string> = {\n  neutral: \"bg-[var(--tbl-well,var(--muted))] text-foreground/75\",\n  success: \"bg-emerald-500/10 text-emerald-700 dark:text-emerald-400\",\n  warning: \"bg-amber-500/[0.12] text-amber-700 dark:text-amber-400\",\n  danger: \"bg-red-500/10 text-red-700 dark:text-red-400\",\n  info: \"bg-[var(--tbl-tint,color-mix(in_oklab,var(--primary)_12%,transparent))] text-[var(--tbl-accent-ink,var(--primary))]\",\n};\n\nexport interface TableBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {\n  tone?: TableBadgeTone;\n  /** A dot before the label, for statuses. Turn it off for plain tags. */\n  dot?: boolean;\n}\n\n/** A status pill (dot + label) or, with `dot={false}`, a tag. The label always shows, so colour is never the only signal. */\nexport const TableBadge = forwardRef<HTMLSpanElement, TableBadgeProps>(\n  ({ tone = \"neutral\", dot = true, className, children, ...props }, ref) => (\n    <span\n      ref={ref}\n      className={cn(\n        \"inline-flex h-6 max-w-40 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-2.5 text-xs font-medium leading-none\",\n        badgeTones[tone],\n        className,\n      )}\n      {...props}\n    >\n      {dot && (\n        <span aria-hidden=\"true\" className=\"size-1.5 shrink-0 rounded-full bg-current\" />\n      )}\n      <span className=\"truncate\">{children}</span>\n    </span>\n  ),\n);\nTableBadge.displayName = \"TableBadge\";\n\n// Initials take one of four hues by a hash of the name, so a person keeps\n// the same colour on every page; the tints mix with the card for both modes.\nconst AVATAR_HUES = [\"#6356d1\", \"#e87ba4\", \"#eda100\", \"#1baf7a\"];\n\nconst hueFor = (seed: string) => {\n  let hash = 0;\n  for (let i = 0; i < seed.length; i += 1) {\n    hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;\n  }\n  return AVATAR_HUES[hash % AVATAR_HUES.length] ?? \"#6356d1\";\n};\n\nconst initialsOf = (name: string) =>\n  name\n    .trim()\n    .split(/\\s+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((part) => part.charAt(0).toUpperCase())\n    .join(\"\") || \"?\";\n\nexport interface TablePersonProps extends React.HTMLAttributes<HTMLDivElement> {\n  name: string;\n  /** A muted second line — an email, a team. */\n  description?: React.ReactNode;\n  /** A photo. Without one (or if it fails to load) the initials show. */\n  src?: string;\n  /** Renders the photo, e.g. `next/image`. Receives src, alt, width, height. */\n  imageComponent?: React.ElementType;\n}\n\n/** Avatar (photo or initials), a name and a muted second line. */\nexport const TablePerson = forwardRef<HTMLDivElement, TablePersonProps>(\n  (\n    { name, description, src, imageComponent: Image = \"img\", className, ...props },\n    ref,\n  ) => {\n    const [failedSrc, setFailedSrc] = useState<string | null>(null);\n    const showImage = Boolean(src) && failedSrc !== src;\n    const hue = hueFor(name);\n    return (\n      <div\n        ref={ref}\n        className={cn(\"flex min-w-0 items-center gap-3\", className)}\n        {...props}\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"grid size-9 shrink-0 select-none place-items-center overflow-hidden rounded-full text-xs font-medium\"\n          style={\n            showImage\n              ? undefined\n              : {\n                  backgroundColor: `color-mix(in oklab, ${hue} 16%, var(--card))`,\n                  color: `color-mix(in oklab, ${hue} 72%, var(--foreground))`,\n                }\n          }\n        >\n          {showImage ? (\n            <Image\n              src={src}\n              alt=\"\"\n              width={36}\n              height={36}\n              loading=\"lazy\"\n              className=\"size-full object-cover\"\n              onError={() => setFailedSrc(src ?? null)}\n            />\n          ) : (\n            initialsOf(name)\n          )}\n        </span>\n        <div className=\"min-w-0\">\n          <p className=\"truncate font-medium leading-5 text-foreground\" title={name}>\n            {name}\n          </p>\n          {description && (\n            <p className=\"truncate text-[13px] leading-5 text-muted-foreground\">\n              {description}\n            </p>\n          )}\n        </div>\n      </div>\n    );\n  },\n);\nTablePerson.displayName = \"TablePerson\";\n\n/** Flattens any nested row into one searchable string. */\nfunction defaultSearchText(value: unknown): string {\n  if (value == null) return \"\";\n  if (typeof value !== \"object\") return String(value);\n  if (Array.isArray(value)) return value.map(defaultSearchText).join(\" \");\n  return Object.values(value as Record<string, unknown>)\n    .map(defaultSearchText)\n    .join(\" \");\n}\n\n/** Works with SQL `id`, Mongo `_id` and anything you map yourself. */\n/** The id fields a row is checked for, in order. */\ntype RowWithId = {\n  id?: string | number;\n  _id?: string | number;\n  uuid?: string | number;\n  key?: string | number;\n};\n\nfunction defaultGetRowId<T>(row: T): string | number {\n  const candidate = row as RowWithId | null | undefined;\n  return (\n    candidate?.id ?? candidate?._id ?? candidate?.uuid ?? candidate?.key ?? \"\"\n  );\n}\n\n/** A filter value matches a field exactly, or any entry of an array field (tags). */\nfunction matchesFilter(field: unknown, value: string): boolean {\n  if (Array.isArray(field)) return field.some((entry) => String(entry) === value);\n  return String(field ?? \"\") === value;\n}\n\nfunction compareValues(a: unknown, b: unknown, type: SortType): number {\n  if (type === \"number\") return Number(a) - Number(b);\n  if (type === \"date\")\n    return new Date(String(a)).getTime() - new Date(String(b)).getTime();\n  if (type === \"auto\" && typeof a === \"number\" && typeof b === \"number\")\n    return a - b;\n  const as = String(a ?? \"\").toLowerCase();\n  const bs = String(b ?? \"\").toLowerCase();\n  return as < bs ? -1 : as > bs ? 1 : 0;\n}\n\n// ==========================================\n// CONTEXT\n// ==========================================\ninterface SortState<T> {\n  id: string;\n  direction: \"asc\" | \"desc\";\n  accessor: Accessor<T>;\n  type: SortType;\n}\n\nconst RowContext = createContext<{ rowId: string | number } | null>(null);\nconst useRowId = () => useContext(RowContext)?.rowId;\n\n// ==========================================\n// ROOT\n// ==========================================\nexport interface DataTableProps<T>\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n  data: T[];\n  /** Defaults to `id`, then `_id`, then `uuid`. */\n  getRowId?: (row: T) => string | number;\n  /** Override how a row is flattened for the search box. */\n  getSearchText?: (row: T) => string;\n  variant?: TableVariant;\n  color?: TableColor;\n  size?: TableSize;\n  initialSort?: { id: string; direction?: \"asc\" | \"desc\"; type?: SortType };\n  pageSize?: number;\n  pageSizeOptions?: number[];\n  /** Fires whenever the selection changes, with the full row objects. */\n  onSelectionChange?: (rows: T[]) => void;\n}\n\nfunction DataTableImpl<T>({\n  data,\n  getRowId = defaultGetRowId,\n  getSearchText,\n  variant = \"default\",\n  color = \"primary\",\n  size = \"md\",\n  initialSort,\n  pageSize: pageSizeProp,\n  pageSizeOptions = [5, 10, 25, 50],\n  onSelectionChange,\n  className,\n  children,\n  ...props\n}: DataTableProps<T>) {\n  const { theme, style } = getThemeStyles(color as string);\n\n  const [search, setSearchState] = useState(\"\");\n  const [sort, setSort] = useState<SortState<T> | null>(\n    initialSort\n      ? {\n          id: initialSort.id,\n          direction: initialSort.direction ?? \"asc\",\n          accessor: initialSort.id,\n          type: initialSort.type ?? \"auto\",\n        }\n      : null,\n  );\n  const [pageState, setPage] = useState(1);\n  const [pageSize, setPageSizeState] = useState(\n    pageSizeProp ?? pageSizeOptions[1] ?? 10,\n  );\n  const [selectedIds, setSelectedIds] = useState<Set<string | number>>(\n    new Set(),\n  );\n  const [expandedIds, setExpandedIds] = useState<Set<string | number>>(\n    new Set(),\n  );\n  const [filterState, setFilterState] = useState<\n    Record<string, { value: string; accessor: Accessor<T> }>\n  >({});\n\n  // 1. Search\n  const searchedRows = useMemo(() => {\n    if (!search.trim()) return data;\n    const needle = search.toLowerCase();\n    return data.filter((row) =>\n      (getSearchText ? getSearchText(row) : defaultSearchText(row))\n        .toLowerCase()\n        .includes(needle),\n    );\n  }, [data, search, getSearchText]);\n\n  // 2. Filters (tabs and filter pills), each an exact match on its accessor\n  const applyFilters = useCallback(\n    (\n      rows: T[],\n      except?: string,\n      extra?: { accessor: Accessor<T>; value: string } | null,\n    ) => {\n      const active = Object.entries(filterState).filter(([id]) => id !== except);\n      if (!active.length && !extra) return rows;\n      return rows.filter(\n        (row) =>\n          active.every(([, filter]) =>\n            matchesFilter(resolveAccessor(row, filter.accessor), filter.value),\n          ) &&\n          (!extra || matchesFilter(resolveAccessor(row, extra.accessor), extra.value)),\n      );\n    },\n    [filterState],\n  );\n\n  const narrowedRows = useMemo(\n    () => applyFilters(searchedRows),\n    [applyFilters, searchedRows],\n  );\n\n  // 3. Sort\n  const filteredRows = useMemo(() => {\n    if (!sort) return narrowedRows;\n    const factor = sort.direction === \"asc\" ? 1 : -1;\n    return [...narrowedRows].sort(\n      (a, b) =>\n        factor *\n        compareValues(\n          resolveAccessor(a, sort.accessor),\n          resolveAccessor(b, sort.accessor),\n          sort.type,\n        ),\n    );\n  }, [narrowedRows, sort]);\n\n  // 4. Paginate\n  const total = filteredRows.length;\n  const totalPages = Math.max(1, Math.ceil(total / pageSize));\n  // The row count can shrink under the current page — a filter, a bigger\n  // page size. Read it clamped rather than correcting it afterwards.\n  const page = Math.min(pageState, totalPages);\n  const rows = useMemo(\n    () => filteredRows.slice((page - 1) * pageSize, page * pageSize),\n    [filteredRows, page, pageSize],\n  );\n\n  const setSearch = useCallback((value: string) => {\n    setSearchState(value);\n    setPage(1);\n  }, []);\n\n  const setPageSize = useCallback((next: number) => {\n    setPageSizeState(next);\n    setPage(1);\n  }, []);\n\n  const setFilter = useCallback(\n    (id: string, accessor: Accessor<T>, value: string | null) => {\n      setFilterState((prev) => {\n        if (value === null) {\n          if (!(id in prev)) return prev;\n          const next = { ...prev };\n          delete next[id];\n          return next;\n        }\n        return { ...prev, [id]: { value, accessor } };\n      });\n      setPage(1);\n    },\n    [],\n  );\n\n  const countFor = useCallback(\n    (id: string, accessor: Accessor<T>, value: string | null) =>\n      applyFilters(searchedRows, id, value === null ? null : { accessor, value })\n        .length,\n    [applyFilters, searchedRows],\n  );\n\n  const filters = useMemo(\n    () =>\n      Object.fromEntries(\n        Object.entries(filterState).map(([id, filter]) => [id, filter.value]),\n      ),\n    [filterState],\n  );\n\n  const resetAll = useCallback(() => {\n    setSearchState(\"\");\n    setFilterState({});\n    setSort(null);\n    setPage(1);\n  }, []);\n\n  const toggleSort = useCallback(\n    (id: string, accessor: Accessor<T>, type: SortType) => {\n      setSort((prev) =>\n        prev && prev.id === id\n          ? {\n              id,\n              accessor,\n              type,\n              direction: prev.direction === \"asc\" ? \"desc\" : \"asc\",\n            }\n          : { id, accessor, type, direction: \"asc\" },\n      );\n    },\n    [],\n  );\n\n  const selectedRows = useMemo(\n    () => data.filter((row) => selectedIds.has(getRowId(row))),\n    [data, selectedIds, getRowId],\n  );\n\n  const selectionCallbackRef = React.useRef(onSelectionChange);\n  const selectedRowsRef = React.useRef(selectedRows);\n  React.useEffect(() => {\n    selectionCallbackRef.current = onSelectionChange;\n    selectedRowsRef.current = selectedRows;\n  });\n\n  useEffect(() => {\n    selectionCallbackRef.current?.(selectedRowsRef.current);\n  }, [selectedIds]);\n\n  const toggleRow = useCallback((id: string | number) => {\n    setSelectedIds((prev) => {\n      const next = new Set(prev);\n      if (next.has(id)) next.delete(id);\n      else next.add(id);\n      return next;\n    });\n  }, []);\n\n  const pageIds = useMemo(() => rows.map(getRowId), [rows, getRowId]);\n\n  const isAllPageSelected =\n    pageIds.length > 0 && pageIds.every((id) => selectedIds.has(id));\n  const isSomePageSelected =\n    !isAllPageSelected && pageIds.some((id) => selectedIds.has(id));\n\n  const toggleAllOnPage = useCallback(() => {\n    setSelectedIds((prev) => {\n      const next = new Set(prev);\n      const allSelected =\n        pageIds.length > 0 && pageIds.every((id) => next.has(id));\n      pageIds.forEach((id) => (allSelected ? next.delete(id) : next.add(id)));\n      return next;\n    });\n  }, [pageIds]);\n\n  const clearSelection = useCallback(() => setSelectedIds(new Set()), []);\n\n  const toggleExpanded = useCallback((id: string | number) => {\n    setExpandedIds((prev) => {\n      const next = new Set(prev);\n      if (next.has(id)) next.delete(id);\n      else next.add(id);\n      return next;\n    });\n  }, []);\n\n  const value: DataTableContextValue<T> = {\n    sourceRows: data,\n    filteredRows,\n    rows,\n    getRowId,\n    search,\n    setSearch,\n    filters,\n    setFilter,\n    countFor,\n    isFiltered: search.trim() !== \"\" || Object.keys(filterState).length > 0,\n    resetAll,\n    sort: sort ? { id: sort.id, direction: sort.direction } : null,\n    toggleSort,\n    page,\n    setPage,\n    pageSize,\n    setPageSize,\n    pageSizeOptions,\n    totalPages,\n    total,\n    from: total === 0 ? 0 : (page - 1) * pageSize + 1,\n    to: Math.min(page * pageSize, total),\n    selectedIds,\n    selectedRows,\n    isSelected: (id) => selectedIds.has(id),\n    toggleRow,\n    toggleAllOnPage,\n    clearSelection,\n    isAllPageSelected,\n    isSomePageSelected,\n    isExpanded: (id) => expandedIds.has(id),\n    toggleExpanded,\n    theme,\n    themeStyle: style,\n  };\n\n  return (\n    <DataTableContext.Provider value={value as DataTableContextValue}>\n      <Table\n        variant={variant}\n        color={color}\n        size={size}\n        className={cn(\"relative w-full\", className)}\n        {...props}\n      >\n        {children}\n      </Table>\n    </DataTableContext.Provider>\n  );\n}\n\n/** Generic over the row shape, so `data` infers T everywhere downstream. */\nexport const DataTable = DataTableImpl as <T>(\n  props: DataTableProps<T>,\n) => React.ReactElement | null;\n\n// ==========================================\n// PASS-THROUGH LAYOUT (unchanged from Table)\n// ==========================================\nexport const DataTableHeading = TableHeading;\nexport const DataTableContent = TableContent;\nexport const DataTableHeader = TableHeader;\nexport const DataTableCell = TableCell;\nexport const DataTableFooter = TableFooter;\nexport const DataTableEmpty = TableEmpty;\n\n// ==========================================\n// WIRED PARTS\n// ==========================================\n/**\n * In the `soft` variant the toolbar is the panel's top band (padded, with a\n * hairline under it) and simply wraps its children — arrange them with your\n * own groups. Elsewhere it is a bare row to place inside `DataTableHeading`.\n */\nexport const DataTableToolbar = forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, children, ...props }, ref) => {\n  const { variant, size } = useTableStyles();\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        variant === \"soft\"\n          ? cn(\n              \"flex w-full flex-wrap items-center gap-x-4 gap-y-3 border-b border-foreground/[0.08]\",\n              softSizeClasses[size].bar,\n            )\n          : \"flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 w-full\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n});\nDataTableToolbar.displayName = \"DataTableToolbar\";\n\n// ------------------------------------------\n// Menus (page size, filters): a listbox under or over a trigger. Arrow keys\n// move between options, Escape closes and returns focus to the trigger.\n// ------------------------------------------\nfunction useMenu() {\n  const [open, setOpen] = useState(false);\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const listRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!open) return;\n    const onPointerDown = (event: MouseEvent) => {\n      if (rootRef.current && !rootRef.current.contains(event.target as Node)) {\n        setOpen(false);\n      }\n    };\n    document.addEventListener(\"mousedown\", onPointerDown);\n    const list = listRef.current;\n    const target =\n      list?.querySelector<HTMLElement>('[aria-selected=\"true\"]') ??\n      list?.querySelector<HTMLElement>('[role=\"option\"]');\n    target?.focus({ preventScroll: true });\n    return () => document.removeEventListener(\"mousedown\", onPointerDown);\n  }, [open]);\n\n  const close = useCallback(() => {\n    setOpen(false);\n    triggerRef.current?.focus({ preventScroll: true });\n  }, []);\n\n  const onListKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    const options = Array.from(\n      listRef.current?.querySelectorAll<HTMLElement>('[role=\"option\"]') ?? [],\n    );\n    const index = options.indexOf(document.activeElement as HTMLElement);\n    const move = (to: number) => {\n      event.preventDefault();\n      options[Math.max(0, Math.min(options.length - 1, to))]?.focus();\n    };\n    if (event.key === \"Escape\") {\n      event.preventDefault();\n      close();\n    } else if (event.key === \"ArrowDown\") move(index + 1);\n    else if (event.key === \"ArrowUp\") move(index - 1);\n    else if (event.key === \"Home\") move(0);\n    else if (event.key === \"End\") move(options.length - 1);\n    else if (event.key === \"Tab\") setOpen(false);\n  };\n\n  return { open, setOpen, rootRef, triggerRef, listRef, close, onListKeyDown };\n}\n\nconst SOFT_MENU =\n  \"absolute z-50 w-max min-w-full max-w-64 rounded-2xl bg-card p-1 text-foreground shadow-xl ring-1 ring-foreground/[0.08]\";\nconst SOFT_OPTION =\n  \"flex h-9 w-full cursor-pointer items-center justify-between gap-3 rounded-xl px-3 text-left text-sm outline-none transition-colors hover:bg-[var(--tbl-well)] focus-visible:bg-[var(--tbl-well)]\";\n// The pill every soft control shares (search, filter, page size, export).\nconst SOFT_PILL =\n  \"bg-[var(--tbl-well)] text-foreground transition-colors hover:bg-[color-mix(in_oklab,var(--foreground)_6%,var(--tbl-well))]\";\nconst SOFT_PILL_ACTIVE =\n  \"bg-[var(--tbl-tint)] text-[var(--tbl-accent-ink)] ring-1 ring-inset ring-[color-mix(in_oklab,var(--tbl-accent)_30%,transparent)] hover:bg-[color-mix(in_oklab,var(--tbl-accent)_14%,var(--card))]\";\n\nexport interface TableSearchProps\n  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"value\" | \"onChange\"> {\n  /** Controlled — you own the search text. */\n  value: string;\n  onValueChange: (value: string) => void;\n  placeholder?: string;\n}\n\n/**\n * A search field in the table's look — a pill with a clear button in the\n * `soft` variant. Controlled, for the plain primitives; inside `DataTable`\n * use `DataTableSearch`, which wires itself up.\n */\nexport const TableSearch = forwardRef<HTMLInputElement, TableSearchProps>(\n  ({ value, onValueChange, placeholder = \"Search…\", className, ...props }, ref) => {\n    const { variant, theme } = useTableStyles();\n\n    if (variant === \"soft\") {\n      // Its own row on phones; from `sm` it shares the row and grows, up to 24rem.\n      return (\n        <div\n          className={cn(\n            \"relative min-w-48 flex-1 basis-full sm:basis-auto md:max-w-sm\",\n            className,\n          )}\n        >\n          <Search\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n          />\n          <input\n            ref={ref}\n            type=\"search\"\n            value={value}\n            onChange={(e) => onValueChange(e.target.value)}\n            placeholder={placeholder}\n            aria-label={placeholder}\n            className=\"h-10 w-full rounded-full border-0 bg-[var(--tbl-well)] pl-10 pr-9 text-sm text-foreground shadow-none outline-none transition-colors placeholder:text-muted-foreground focus:bg-card focus:ring-2 focus:ring-[color-mix(in_oklab,var(--tbl-accent)_50%,transparent)] [&::-webkit-search-cancel-button]:appearance-none\"\n            {...props}\n          />\n          {value && (\n            <button\n              type=\"button\"\n              onClick={() => onValueChange(\"\")}\n              aria-label=\"Clear search\"\n              className={cn(\n                \"absolute right-2.5 top-1/2 grid size-6 -translate-y-1/2 place-items-center rounded-full text-muted-foreground transition-colors hover:bg-card hover:text-foreground\",\n                FOCUS_RING,\n              )}\n            >\n              <X className=\"size-3.5\" aria-hidden=\"true\" />\n            </button>\n          )}\n        </div>\n      );\n    }\n\n    return (\n      <div className={cn(\"relative w-full sm:w-[260px]\", className)}>\n        <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none\" />\n        <input\n          ref={ref}\n          type=\"search\"\n          value={value}\n          onChange={(e) => onValueChange(e.target.value)}\n          placeholder={placeholder}\n          className={cn(\n            \"w-full h-9 pl-9 pr-3 py-1.5 text-[13px] bg-background text-foreground border rounded-lg outline-none transition-all shadow-sm focus:border-primary/30\",\n            theme.borderSoft || \"border-border\",\n            theme.hoverBorder,\n            theme.focusRing,\n          )}\n          {...props}\n        />\n      </div>\n    );\n  },\n);\nTableSearch.displayName = \"TableSearch\";\n\nexport type DataTableSearchProps = Omit<TableSearchProps, \"value\" | \"onValueChange\">;\n\n/** `TableSearch` wired to the DataTable's search. */\nexport const DataTableSearch = forwardRef<\n  HTMLInputElement,\n  DataTableSearchProps\n>((props, ref) => {\n  const { search, setSearch } = useDataTable();\n  return <TableSearch ref={ref} value={search} onValueChange={setSearch} {...props} />;\n});\nDataTableSearch.displayName = \"DataTableSearch\";\n\nexport interface DataTablePageSizeProps {\n  label?: string;\n  suffix?: string;\n  className?: string;\n  /**\n   * Which way the menu opens. Defaults to `top` in the `soft` variant, where\n   * the picker sits in the footer, and to `bottom` elsewhere.\n   */\n  side?: \"top\" | \"bottom\";\n}\n\nexport const DataTablePageSize = forwardRef<\n  HTMLDivElement,\n  DataTablePageSizeProps\n>(({ label = \"Show\", suffix = \"Entries\", className, side }, ref) => {\n  const { pageSize, setPageSize, pageSizeOptions, theme, themeStyle } =\n    useDataTable();\n  const { variant } = useTableStyles();\n  const {\n    open: menuOpen,\n    setOpen: setMenuOpen,\n    rootRef: menuRootRef,\n    triggerRef: menuTriggerRef,\n    listRef: menuListRef,\n    close: closeMenu,\n    onListKeyDown: onMenuKeyDown,\n  } = useMenu();\n  const listId = useId();\n  const [isOpen, setIsOpen] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        containerRef.current &&\n        !containerRef.current.contains(event.target as Node)\n      ) {\n        setIsOpen(false);\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, []);\n\n  if (variant === \"soft\") {\n    const opensUp = (side ?? \"top\") === \"top\";\n    return (\n      <div\n        ref={ref}\n        className={cn(\"flex items-center gap-2 text-sm text-muted-foreground\", className)}\n      >\n        {label && <span className=\"hidden whitespace-nowrap sm:inline\">{label}</span>}\n        <div ref={menuRootRef} className=\"relative\">\n          <button\n            ref={menuTriggerRef}\n            type=\"button\"\n            onClick={() => setMenuOpen((open) => !open)}\n            aria-haspopup=\"listbox\"\n            aria-expanded={menuOpen}\n            aria-controls={menuOpen ? listId : undefined}\n            aria-label={label ? `${label}: ${pageSize}` : `Rows per page: ${pageSize}`}\n            className={cn(\n              \"flex h-9 min-w-[4.75rem] items-center justify-between gap-1 rounded-full pl-3.5 pr-2 text-sm tabular-nums\",\n              SOFT_PILL,\n              FOCUS_RING,\n            )}\n          >\n            <span>{pageSize}</span>\n            <ChevronDown\n              aria-hidden=\"true\"\n              className={cn(\n                \"size-4 text-muted-foreground transition-transform duration-200\",\n                menuOpen && \"rotate-180\",\n              )}\n            />\n          </button>\n          {menuOpen && (\n            <div\n              ref={menuListRef}\n              id={listId}\n              role=\"listbox\"\n              aria-label={label || \"Rows per page\"}\n              onKeyDown={onMenuKeyDown}\n              className={cn(SOFT_MENU, \"left-0\", opensUp ? \"bottom-full mb-2\" : \"top-full mt-2\")}\n            >\n              {pageSizeOptions.map((option) => {\n                const selected = pageSize === option;\n                return (\n                  <button\n                    key={option}\n                    type=\"button\"\n                    role=\"option\"\n                    aria-selected={selected}\n                    onClick={() => {\n                      setPageSize(option);\n                      closeMenu();\n                    }}\n                    className={cn(\n                      SOFT_OPTION,\n                      \"tabular-nums\",\n                      selected && \"font-medium text-[var(--tbl-accent-ink)]\",\n                    )}\n                  >\n                    {option}\n                    {selected && <Check aria-hidden=\"true\" className=\"size-4\" />}\n                  </button>\n                );\n              })}\n            </div>\n          )}\n        </div>\n        {suffix && <span className=\"hidden whitespace-nowrap sm:inline\">{suffix}</span>}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        \"flex items-center gap-2 text-[13px] text-muted-foreground\",\n        className,\n      )}\n    >\n      {label && <span className=\"font-medium text-foreground/80\">{label}</span>}\n      <div ref={containerRef} className=\"relative inline-block text-left\">\n        <button\n          type=\"button\"\n          onClick={() => setIsOpen((prev) => !prev)}\n          aria-expanded={isOpen}\n          aria-haspopup=\"listbox\"\n          style={themeStyle}\n          className={cn(\n            \"h-9 pl-3 pr-2.5 text-xs font-semibold bg-background text-foreground border rounded-lg flex items-center gap-2 outline-none cursor-pointer transition-all shadow-sm\",\n            theme.borderSoft || \"border-border\",\n            theme.hoverBorder,\n            theme.focusRing,\n            isOpen && \"border-primary/30\",\n          )}\n        >\n          <span>{pageSize}</span>\n          <ChevronDown\n            className={cn(\n              \"w-3.5 h-3.5 text-muted-foreground transition-transform duration-200\",\n              isOpen && \"rotate-180 text-primary\",\n            )}\n          />\n        </button>\n\n        {isOpen && (\n          <div className=\"absolute left-0 mt-1.5 min-w-[70px] rounded-xl border border-border bg-popover text-popover-foreground shadow-lg ring-1 ring-black/5 z-50 py-1 focus:outline-none animate-in fade-in-50 zoom-in-95 duration-100\">\n            {pageSizeOptions.map((opt) => {\n              const isSelected = pageSize === opt;\n              return (\n                <button\n                  key={opt}\n                  type=\"button\"\n                  onClick={() => {\n                    setPageSize(opt);\n                    setIsOpen(false);\n                  }}\n                  style={isSelected ? themeStyle : undefined}\n                  className={cn(\n                    \"flex items-center justify-between w-full px-3 py-1.5 text-xs font-medium transition-colors text-left cursor-pointer\",\n                    isSelected\n                      ? cn(theme.bgSoft, theme.text, \"font-semibold\")\n                      : cn(\"hover:bg-muted text-foreground/80\", theme.hoverBgSoft),\n                  )}\n                >\n                  <span>{opt}</span>\n                  {isSelected && <Check className=\"w-3.5 h-3.5 shrink-0\" strokeWidth={2.5} />}\n                </button>\n              );\n            })}\n          </div>\n        )}\n      </div>\n      {suffix && <span className=\"font-medium text-foreground/80\">{suffix}</span>}\n    </div>\n  );\n});\nDataTablePageSize.displayName = \"DataTablePageSize\";\n\nexport interface DataTableExportProps<T = unknown> {\n  filename?: string;\n  formats?: (\"csv\" | \"xlsx\" | \"json\")[];\n  /** Custom data formatter before export. If not provided, raw row objects are exported. */\n  formatData?: (rows: T[]) => Record<string, unknown>[];\n  color?: TableColor;\n  className?: string;\n}\n\nexport function DataTableExport<T = unknown>({\n  filename = \"export\",\n  formats = [\"csv\", \"xlsx\"],\n  formatData,\n  color,\n  className,\n}: DataTableExportProps<T>) {\n  const { filteredRows, selectedRows, theme: defaultTheme, themeStyle: defaultStyle } = useDataTable<T>();\n  const soft = useTableStyles().variant === \"soft\";\n  const [isOpen, setIsOpen] = useState(false);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n  const { theme, style } = color ? getThemeStyles(color as string) : { theme: defaultTheme, style: defaultStyle };\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {\n        setIsOpen(false);\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, []);\n\n  const getExportableData = (): Record<string, unknown>[] => {\n    const source = selectedRows.length > 0 ? selectedRows : filteredRows;\n    if (formatData) return formatData(source);\n    return source.map((row) => {\n      if (typeof row === \"object\" && row !== null) {\n        const cleanRow: Record<string, unknown> = {};\n        Object.entries(row as Record<string, unknown>).forEach(([key, val]) => {\n          if (typeof val !== \"function\" && typeof val !== \"symbol\") {\n            cleanRow[key] = typeof val === \"object\" && val !== null ? JSON.stringify(val) : val;\n          }\n        });\n        return cleanRow;\n      }\n      return { value: row };\n    });\n  };\n\n  const exportCSV = () => {\n    const data = getExportableData();\n    if (data.length === 0) return;\n\n    const headers = Array.from(new Set(data.flatMap((item) => Object.keys(item))));\n    const csvRows = [\n      headers.join(\",\"),\n      ...data.map((row) =>\n        headers\n          .map((fieldName) => {\n            const val = row[fieldName] ?? \"\";\n            const escaped = String(val).replace(/\"/g, '\"\"');\n            return `\"${escaped}\"`;\n          })\n          .join(\",\"),\n      ),\n    ];\n\n    const blob = new Blob([\"\\uFEFF\" + csvRows.join(\"\\n\")], { type: \"text/csv;charset=utf-8;\" });\n    const url = URL.createObjectURL(blob);\n    const link = document.createElement(\"a\");\n    link.setAttribute(\"href\", url);\n    link.setAttribute(\"download\", `${filename}.csv`);\n    document.body.appendChild(link);\n    link.click();\n    document.body.removeChild(link);\n    URL.revokeObjectURL(url);\n    setIsOpen(false);\n  };\n\n  const exportXLSX = () => {\n    const data = getExportableData();\n    if (data.length === 0) return;\n\n    const headers = Array.from(new Set(data.flatMap((item) => Object.keys(item))));\n    const xmlHeader = `<?xml version=\"1.0\"?><?mso-application progid=\"Excel.Sheet\"?>\n<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\"\n xmlns:o=\"urn:schemas-microsoft-com:office:office\"\n xmlns:x=\"urn:schemas-microsoft-com:office:excel\"\n xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">\n<Worksheet ss:Name=\"Sheet1\">\n<Table>`;\n\n    const xmlFooter = `</Table></Worksheet></Workbook>`;\n\n    const headerRowXml = `<Row>${headers\n      .map((h) => `<Cell><Data ss:Type=\"String\">${String(h).replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\")}</Data></Cell>`)\n      .join(\"\")}</Row>`;\n\n    const dataRowsXml = data\n      .map(\n        (row) =>\n          `<Row>${headers\n            .map((h) => {\n              const val = row[h] ?? \"\";\n              const isNum = typeof val === \"number\";\n              const escapedVal = String(val)\n                .replace(/&/g, \"&amp;\")\n                .replace(/</g, \"&lt;\")\n                .replace(/>/g, \"&gt;\");\n              return `<Cell><Data ss:Type=\"${isNum ? \"Number\" : \"String\"}\">${escapedVal}</Data></Cell>`;\n            })\n            .join(\"\")}</Row>`,\n      )\n      .join(\"\");\n\n    const excelXml = xmlHeader + headerRowXml + dataRowsXml + xmlFooter;\n\n    const blob = new Blob([excelXml], { type: \"application/vnd.ms-excel\" });\n    const url = URL.createObjectURL(blob);\n    const link = document.createElement(\"a\");\n    link.setAttribute(\"href\", url);\n    link.setAttribute(\"download\", `${filename}.xls`);\n    document.body.appendChild(link);\n    link.click();\n    document.body.removeChild(link);\n    URL.revokeObjectURL(url);\n    setIsOpen(false);\n  };\n\n  const exportJSON = () => {\n    const data = getExportableData();\n    if (data.length === 0) return;\n\n    const blob = new Blob([JSON.stringify(data, null, 2)], { type: \"application/json\" });\n    const url = URL.createObjectURL(blob);\n    const link = document.createElement(\"a\");\n    link.setAttribute(\"href\", url);\n    link.setAttribute(\"download\", `${filename}.json`);\n    document.body.appendChild(link);\n    link.click();\n    document.body.removeChild(link);\n    URL.revokeObjectURL(url);\n    setIsOpen(false);\n  };\n\n  const isSelectedOnly = selectedRows.length > 0;\n  const count = isSelectedOnly ? selectedRows.length : filteredRows.length;\n\n  return (\n    <div ref={dropdownRef} className={cn(\"relative inline-block text-left\", className)}>\n      <button\n        type=\"button\"\n        onClick={() => setIsOpen((prev) => !prev)}\n        disabled={count === 0}\n        aria-expanded={isOpen}\n        aria-haspopup=\"true\"\n        style={style}\n        className={cn(\n          soft\n            ? cn(\n                \"inline-flex h-10 cursor-pointer items-center gap-2 rounded-full px-4 text-sm font-medium disabled:cursor-not-allowed disabled:opacity-40\",\n                SOFT_PILL,\n                FOCUS_RING,\n              )\n            : cn(\n                \"inline-flex items-center gap-1.5 h-9 px-3 text-xs font-semibold rounded-lg border bg-background text-foreground transition-all duration-150 shadow-sm disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer\",\n                theme.borderSoft || \"border-border\",\n                theme.hoverBorder,\n                theme.focusRing,\n              ),\n        )}\n      >\n        <Download className={cn(\"text-muted-foreground\", soft ? \"size-4\" : \"w-3.5 h-3.5\")} />\n        <span>Export</span>\n        {isSelectedOnly && (\n          <span className=\"ml-0.5 px-1.5 py-0.2 rounded-full text-[10px] font-semibold bg-primary/10 text-primary\">\n            {count}\n          </span>\n        )}\n        <ChevronDown className={cn(\"w-3 h-3 text-muted-foreground transition-transform duration-200\", isOpen && \"rotate-180\")} />\n      </button>\n\n      {isOpen && (\n        <div\n          className={cn(\n            soft\n              ? \"absolute right-0 top-full z-50 mt-2 w-44 rounded-2xl bg-card p-1 text-foreground shadow-xl ring-1 ring-foreground/[0.08]\"\n              : \"absolute right-0 mt-1.5 w-36 rounded-xl border border-border bg-popover text-popover-foreground shadow-lg ring-1 ring-black/5 z-50 py-1 focus:outline-none animate-in fade-in-50 zoom-in-95 duration-100\",\n          )}\n        >\n          <div className=\"px-3 py-1.5 text-[10px] font-semibold text-muted-foreground uppercase tracking-wider border-b border-border/50\">\n            {isSelectedOnly ? `Export ${count} Selected` : `Export All (${count})`}\n          </div>\n          {formats.includes(\"csv\") && (\n            <button\n              type=\"button\"\n              onClick={exportCSV}\n              className={cn(\n                \"flex items-center justify-between w-full px-3 py-2 text-xs font-medium transition-colors text-left cursor-pointer hover:bg-muted text-foreground/80\",\n                theme.hoverBgSoft,\n                soft && \"h-9 rounded-xl py-0 text-sm text-foreground hover:bg-[var(--tbl-well)]\",\n              )}\n            >\n              <span>Export CSV</span>\n              <span className=\"text-[10px] text-muted-foreground font-mono uppercase\">.csv</span>\n            </button>\n          )}\n          {(formats.includes(\"xlsx\") || formats.includes(\"xls\" as never)) && (\n            <button\n              type=\"button\"\n              onClick={exportXLSX}\n              className={cn(\n                \"flex items-center justify-between w-full px-3 py-2 text-xs font-medium transition-colors text-left cursor-pointer hover:bg-muted text-foreground/80\",\n                theme.hoverBgSoft,\n                soft && \"h-9 rounded-xl py-0 text-sm text-foreground hover:bg-[var(--tbl-well)]\",\n              )}\n            >\n              <span>Export Excel</span>\n              <span className=\"text-[10px] text-muted-foreground font-mono uppercase\">.xlsx</span>\n            </button>\n          )}\n          {formats.includes(\"json\") && (\n            <button\n              type=\"button\"\n              onClick={exportJSON}\n              className={cn(\n                \"flex items-center justify-between w-full px-3 py-2 text-xs font-medium transition-colors text-left cursor-pointer hover:bg-muted text-foreground/80\",\n                theme.hoverBgSoft,\n                soft && \"h-9 rounded-xl py-0 text-sm text-foreground hover:bg-[var(--tbl-well)]\",\n              )}\n            >\n              <span>Export JSON</span>\n              <span className=\"text-[10px] text-muted-foreground font-mono uppercase\">.json</span>\n            </button>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport interface DataTableBodyProps<T = unknown>\n  extends Omit<React.HTMLAttributes<HTMLTableSectionElement>, \"children\"> {\n  children: ((rows: T[]) => React.ReactNode) | React.ReactNode;\n  animate?: boolean;\n}\n\nfunction DataTableBodyImpl<T>({\n  children,\n  animate = false,\n  ...props\n}: DataTableBodyProps<T>) {\n  const { rows, page, sort, search } = useDataTable<T>();\n  return (\n    <TableBody\n      animate={animate}\n      animateKey={`${page}-${sort?.id ?? \"\"}-${sort?.direction ?? \"\"}-${search}`}\n      {...props}\n    >\n      {typeof children === \"function\"\n        ? (children as (rows: T[]) => React.ReactNode)(rows)\n        : children}\n    </TableBody>\n  );\n}\n\nexport const DataTableBody = DataTableBodyImpl as <T = unknown>(\n  props: DataTableBodyProps<T>,\n) => React.ReactElement | null;\n\nexport interface DataTableRowProps<T = unknown>\n  extends React.HTMLAttributes<HTMLTableRowElement> {\n  /** Pass the row and selection/expansion wire themselves up. */\n  row?: T;\n  /** Or pass the id directly. */\n  rowId?: string | number;\n  isHeader?: boolean;\n}\n\nfunction DataTableRowImpl<T>({\n  row,\n  rowId: rowIdProp,\n  isHeader = false,\n  children,\n  ...props\n}: DataTableRowProps<T>) {\n  const { getRowId, isSelected } = useDataTable<T>();\n  const rowId =\n    rowIdProp ??\n    (row !== undefined ? getRowId(row) : (undefined as unknown as string | number));\n\n  const content = (\n    <TableRow\n      isHeader={isHeader}\n      selected={rowId !== undefined && isSelected(rowId)}\n      {...props}\n    >\n      {children}\n    </TableRow>\n  );\n\n  if (rowId === undefined) return content;\n  return (\n    <RowContext.Provider value={{ rowId }}>{content}</RowContext.Provider>\n  );\n}\n\nexport const DataTableRow = DataTableRowImpl as <T = unknown>(\n  props: DataTableRowProps<T>,\n) => React.ReactElement | null;\n\nexport interface DataTableHeadProps<T = unknown>\n  extends Omit<React.ThHTMLAttributes<HTMLTableCellElement>, \"align\"> {\n  align?: TableAlign;\n  /** Dot-path or accessor function. Presence of this makes the header sortable. */\n  sortBy?: Accessor<T>;\n  /** Required only when `sortBy` is a function. */\n  sortId?: string;\n  sortType?: SortType;\n  /** Pin the column to the right edge (pair it with `sticky` on its cells). */\n  sticky?: boolean;\n}\n\nfunction DataTableHeadImpl<T>({\n  align = \"left\",\n  sortBy,\n  sortId,\n  sortType = \"auto\",\n  children,\n  ...props\n}: DataTableHeadProps<T>) {\n  const { sort, toggleSort } = useDataTable<T>();\n  const autoId = React.useId();\n  const id =\n    typeof sortBy === \"string\" ? sortBy : (sortId ?? autoId);\n\n  return (\n    <TableHead\n      align={align}\n      sortable={sortBy !== undefined}\n      sortDirection={sort?.id === id ? sort.direction : null}\n      onSort={() => sortBy !== undefined && toggleSort(id, sortBy, sortType)}\n      {...props}\n    >\n      {children}\n    </TableHead>\n  );\n}\n\nexport const DataTableHead = DataTableHeadImpl as <T = unknown>(\n  props: DataTableHeadProps<T>,\n) => React.ReactElement | null;\n\nfunction CheckBox({\n  checked,\n  indeterminate,\n  onClick,\n  label,\n}: {\n  checked: boolean;\n  indeterminate?: boolean;\n  onClick: () => void;\n  label: string;\n}) {\n  const { theme } = useDataTable();\n  const { variant } = useTableStyles();\n  const active = checked || indeterminate;\n\n  if (variant === \"soft\") {\n    return (\n      <button\n        type=\"button\"\n        role=\"checkbox\"\n        aria-checked={indeterminate ? \"mixed\" : checked}\n        aria-label={label}\n        onClick={(e) => {\n          e.stopPropagation();\n          onClick();\n        }}\n        className={cn(\n          // A 38px hit area around an 18px box.\n          \"relative grid size-[18px] shrink-0 place-items-center rounded-[5px] border transition-colors after:absolute after:-inset-2.5 after:content-['']\",\n          FOCUS_RING,\n          active\n            ? cn(theme.bg, \"border-transparent\")\n            : \"border-foreground/25 bg-card hover:border-foreground/45\",\n        )}\n      >\n        {indeterminate ? (\n          <Minus className=\"size-3.5\" strokeWidth={3} aria-hidden=\"true\" />\n        ) : checked ? (\n          <Check className=\"size-3.5\" strokeWidth={3} aria-hidden=\"true\" />\n        ) : null}\n      </button>\n    );\n  }\n\n  return (\n    <button\n      type=\"button\"\n      role=\"checkbox\"\n      aria-checked={indeterminate ? \"mixed\" : checked}\n      aria-label={label}\n      onClick={(e) => {\n        e.stopPropagation();\n        onClick();\n      }}\n      className={cn(\n        \"w-4 h-4 rounded border-[1.5px] flex items-center justify-center transition-colors shrink-0\",\n        active ? cn(theme.bg, theme.border) : cn(\"bg-background\", theme.borderSoft || \"border-border\"),\n      )}\n    >\n      {indeterminate ? (\n        <Minus className=\"w-2.5 h-2.5\" strokeWidth={3} />\n      ) : checked ? (\n        <Check className=\"w-2.5 h-2.5\" strokeWidth={3} />\n      ) : null}\n    </button>\n  );\n}\n\nexport function DataTableSelectAll({ className }: { className?: string }) {\n  const { isAllPageSelected, isSomePageSelected, toggleAllOnPage } =\n    useDataTable();\n  return (\n    <div className={cn(\"flex items-center justify-center\", className)}>\n      <CheckBox\n        checked={isAllPageSelected}\n        indeterminate={isSomePageSelected}\n        onClick={toggleAllOnPage}\n        label=\"Select all rows on this page\"\n      />\n    </div>\n  );\n}\n\nexport function DataTableSelect({\n  label = \"Select row\",\n  className,\n}: {\n  /** Accessible name, e.g. \"Select Priya Raman\". */\n  label?: string;\n  className?: string;\n}) {\n  const { isSelected, toggleRow } = useDataTable();\n  const rowId = useRowId();\n  if (rowId === undefined) return null;\n  return (\n    <div className={cn(\"flex items-center justify-center\", className)}>\n      <CheckBox\n        checked={isSelected(rowId)}\n        onClick={() => toggleRow(rowId)}\n        label={label}\n      />\n    </div>\n  );\n}\n\nexport function DataTableExpandToggle({ className }: { className?: string }) {\n  const { isExpanded, toggleExpanded } = useDataTable();\n  const rowId = useRowId();\n  if (rowId === undefined) return null;\n  return (\n    <TableExpandToggle\n      open={isExpanded(rowId)}\n      onClick={(e) => {\n        e.stopPropagation();\n        toggleExpanded(rowId);\n      }}\n      className={className}\n    />\n  );\n}\n\nexport interface DataTableExpandedRowProps\n  extends React.HTMLAttributes<HTMLTableRowElement> {\n  rowId: string | number;\n  colSpan: number;\n}\n\nexport function DataTableExpandedRow({\n  rowId,\n  colSpan,\n  children,\n  ...props\n}: DataTableExpandedRowProps) {\n  const { isExpanded } = useDataTable();\n  return (\n    <TableExpandedRow open={isExpanded(rowId)} colSpan={colSpan} {...props}>\n      {children}\n    </TableExpandedRow>\n  );\n}\n\nexport interface DataTableBatchBarProps<T = unknown> {\n  /** Receives the selected rows and a way to clear them. */\n  children?:\n    | ((args: {\n        selectedRows: T[];\n        selectedIds: (string | number)[];\n        clearSelection: () => void;\n      }) => React.ReactNode)\n    | React.ReactNode;\n  floating?: boolean;\n  /** Singular and plural, for \"3 users selected\". */\n  noun?: [string, string];\n  className?: string;\n}\n\nexport function DataTableBatchBar<T = unknown>({\n  children,\n  floating = true,\n  noun,\n  className,\n}: DataTableBatchBarProps<T>) {\n  const { selectedIds, selectedRows, clearSelection, theme, themeStyle } =\n    useDataTable<T>();\n  const { variant } = useTableStyles();\n  const count = selectedIds.size;\n  const content =\n    typeof children === \"function\"\n      ? children({\n          selectedRows,\n          selectedIds: Array.from(selectedIds),\n          clearSelection,\n        })\n      : children;\n\n  if (variant === \"soft\") {\n    return (\n      <div\n        className={cn(\n          \"pointer-events-none z-30 flex justify-center px-4 transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none\",\n          floating ? \"absolute inset-x-0 bottom-5\" : \"relative\",\n          count > 0 ? \"translate-y-0 opacity-100\" : \"translate-y-3 opacity-0\",\n          className,\n        )}\n        inert={count === 0}\n        aria-hidden={count === 0}\n      >\n        <div\n          role=\"region\"\n          aria-label=\"Actions for selected rows\"\n          className=\"pointer-events-auto flex max-w-full items-center gap-1 rounded-full bg-[#1b2230] p-1.5 pl-4 text-white shadow-[0_18px_40px_-12px_rgb(15_23_42/0.5)] dark:bg-[#334155] dark:ring-1 dark:ring-white/[0.08]\"\n        >\n          <p\n            className=\"whitespace-nowrap pr-1 text-sm font-medium tabular-nums\"\n            aria-live=\"polite\"\n          >\n            {count}\n            {noun && (\n              <span className=\"hidden sm:inline\">\n                {\" \"}\n                {count === 1 ? noun[0] : noun[1]}\n              </span>\n            )}{\" \"}\n            selected\n          </p>\n          <span aria-hidden=\"true\" className=\"mx-1.5 h-5 w-px shrink-0 bg-white/15\" />\n          <div className=\"flex items-center gap-0.5\">{content}</div>\n          <button\n            type=\"button\"\n            onClick={clearSelection}\n            aria-label=\"Clear selection\"\n            title=\"Clear selection\"\n            className=\"ml-0.5 grid size-8 shrink-0 place-items-center rounded-full text-white/70 outline-none transition-colors hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:ring-white/60\"\n          >\n            <X className=\"size-4\" aria-hidden=\"true\" />\n          </button>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-3 py-1.5 sm:py-2 rounded-xl shadow-lg border border-white/20 transition-all duration-300 ease-out z-30\",\n        theme.bg,\n        floating &&\n          \"absolute left-1/2 -translate-x-1/2 bottom-4 pointer-events-auto\",\n        count > 0\n          ? \"opacity-100 pointer-events-auto translate-y-0 scale-100\"\n          : \"opacity-0 pointer-events-none translate-y-2 scale-95\",\n        className,\n      )}\n      style={themeStyle}\n      inert={count === 0}\n      aria-hidden={count === 0}\n    >\n      <span className=\"text-[11px] sm:text-[12px] font-bold whitespace-nowrap pl-0.5\">\n        {count}\n        {noun ? ` ${count === 1 ? noun[0] : noun[1]}` : \"\"} selected\n      </span>\n      <div className=\"w-px h-[16px] bg-current opacity-30 shrink-0 mx-0.5\" />\n      {content}\n      <div className=\"w-px h-[16px] bg-current opacity-30 shrink-0 mx-0.5\" />\n      <button\n        type=\"button\"\n        onClick={clearSelection}\n        title=\"Clear selection\"\n        aria-label=\"Clear selection\"\n        className=\"p-1 text-current opacity-80 hover:opacity-100 transition-opacity shrink-0 cursor-pointer\"\n      >\n        <X className=\"w-3.5 h-3.5\" />\n      </button>\n    </div>\n  );\n}\n\nexport interface DataTableBatchActionProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** `danger` for destructive actions (delete). */\n  tone?: \"default\" | \"danger\";\n}\n\n/** A button inside `DataTableBatchBar`. Icons passed as children are sized for you. */\nexport const DataTableBatchAction = forwardRef<\n  HTMLButtonElement,\n  DataTableBatchActionProps\n>(({ tone = \"default\", type = \"button\", className, children, ...props }, ref) => {\n  const { variant } = useTableStyles();\n  return (\n    <button\n      ref={ref}\n      type={type}\n      className={cn(\n        variant === \"soft\"\n          ? cn(\n              \"inline-flex h-8 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-3 text-[13px] font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-white/60 disabled:opacity-50 [&_svg]:size-3.5\",\n              tone === \"danger\"\n                ? \"text-red-300 hover:bg-red-400/15\"\n                : \"text-white hover:bg-white/10\",\n            )\n          : cn(\n              \"flex items-center gap-1 px-2 py-1 rounded-md text-[11px] sm:text-[12px] font-semibold whitespace-nowrap transition-colors cursor-pointer disabled:opacity-50 [&_svg]:w-3 [&_svg]:h-3\",\n              tone === \"danger\"\n                ? \"bg-black/20 hover:bg-black/30\"\n                : \"bg-current/20 hover:bg-current/30\",\n            ),\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n});\nDataTableBatchAction.displayName = \"DataTableBatchAction\";\n\nexport interface TableInfoProps {\n  /** First row shown (1-based; 0 when there are none). */\n  from: number;\n  /** Last row shown. */\n  to: number;\n  total: number;\n  className?: string;\n}\n\n/** \"Showing 1–10 of 57\" for the footer. `DataTableInfo` fills it in for you. */\nexport function TableInfo({ from, to, total, className }: TableInfoProps) {\n  const { variant } = useTableStyles();\n  if (variant === \"soft\") {\n    return (\n      <p className={cn(\"text-sm tabular-nums text-muted-foreground\", className)}>\n        Showing <span className=\"font-medium text-foreground\">{from}</span>\n        {\"–\"}\n        <span className=\"font-medium text-foreground\">{to}</span> of{\" \"}\n        <span className=\"font-medium text-foreground\">{total}</span>\n      </p>\n    );\n  }\n  return (\n    <div className={cn(\"text-[13px] text-muted-foreground\", className)}>\n      Showing <strong className=\"font-semibold text-foreground\">{from}</strong>{\" \"}\n      to <strong className=\"font-semibold text-foreground\">{to}</strong> of{\" \"}\n      <strong className=\"font-semibold text-foreground\">{total}</strong> results\n    </div>\n  );\n}\n\n/** `TableInfo` wired to the DataTable's paging. */\nexport function DataTableInfo({ className }: { className?: string }) {\n  const { from, to, total } = useDataTable();\n  return <TableInfo from={from} to={to} total={total} className={className} />;\n}\n\n/** First, last and the pages beside the current one, with gaps as \"…\". */\nfunction pageList(page: number, totalPages: number): (number | \"gap\")[] {\n  if (totalPages <= 7) {\n    return Array.from({ length: totalPages }, (_, i) => i + 1);\n  }\n  const left = Math.max(page - 1, 1);\n  const right = Math.min(page + 1, totalPages);\n  const leftGap = left > 2;\n  const rightGap = right < totalPages - 1;\n  if (!leftGap && rightGap) return [1, 2, 3, 4, 5, \"gap\", totalPages];\n  if (leftGap && !rightGap) {\n    return [1, \"gap\", ...Array.from({ length: 5 }, (_, i) => totalPages - 4 + i)];\n  }\n  return [1, \"gap\", left, page, right, \"gap\", totalPages];\n}\n\nexport interface TablePaginationProps {\n  /** Controlled — the current page, 1-based. */\n  page: number;\n  totalPages: number;\n  onPageChange: (page: number) => void;\n  className?: string;\n}\n\n/**\n * Previous, page numbers and next. In the `soft` variant: pills, with the\n * first and last page and \"…\" gaps once there are more than seven pages.\n * `DataTablePagination` wires it to a DataTable.\n */\nexport function TablePagination({\n  page,\n  totalPages: totalPagesProp,\n  onPageChange: setPage,\n  className,\n}: TablePaginationProps) {\n  const { variant, theme } = useTableStyles();\n  const totalPages = Math.max(1, totalPagesProp);\n\n  if (variant === \"soft\") {\n    // Previous / next disable at the ends rather than vanish, so the row never\n    // shifts under the pointer.\n    const step = cn(\n      \"inline-flex size-8 items-center justify-center rounded-full text-foreground ring-1 ring-inset ring-foreground/[0.08] transition-colors hover:bg-[var(--tbl-well)] disabled:pointer-events-none disabled:opacity-40 sm:size-9\",\n      FOCUS_RING,\n    );\n    return (\n      <nav aria-label=\"Pagination\" className={cn(\"flex flex-wrap items-center justify-center gap-0.5 sm:gap-1\", className)}>\n        <button\n          type=\"button\"\n          onClick={() => setPage(Math.max(1, page - 1))}\n          disabled={page <= 1}\n          aria-label=\"Previous page\"\n          className={step}\n        >\n          <ChevronLeft className=\"size-4\" aria-hidden=\"true\" />\n        </button>\n        {pageList(page, totalPages).map((entry, index) =>\n          entry === \"gap\" ? (\n            <span\n              key={`gap-${index}`}\n              aria-hidden=\"true\"\n              className=\"inline-flex h-8 min-w-6 items-center justify-center text-sm text-muted-foreground sm:h-9 sm:min-w-9\"\n            >\n              …\n            </span>\n          ) : (\n            <button\n              key={entry}\n              type=\"button\"\n              onClick={() => setPage(entry)}\n              aria-current={entry === page ? \"page\" : undefined}\n              aria-label={`Page ${entry}`}\n              className={cn(\n                \"inline-flex h-8 min-w-8 items-center justify-center rounded-full px-2 text-sm font-medium tabular-nums transition-colors sm:h-9 sm:min-w-9 sm:px-2.5\",\n                FOCUS_RING,\n                entry === page\n                  ? cn(theme.bg, \"shadow-sm\")\n                  : \"text-muted-foreground hover:bg-[var(--tbl-well)] hover:text-foreground\",\n              )}\n            >\n              {entry}\n            </button>\n          ),\n        )}\n        <button\n          type=\"button\"\n          onClick={() => setPage(Math.min(totalPages, page + 1))}\n          disabled={page >= totalPages}\n          aria-label=\"Next page\"\n          className={step}\n        >\n          <ChevronRight className=\"size-4\" aria-hidden=\"true\" />\n        </button>\n      </nav>\n    );\n  }\n\n  const windowSize = Math.min(5, totalPages);\n  const start = Math.max(\n    1,\n    Math.min(page - Math.floor(windowSize / 2), totalPages - windowSize + 1),\n  );\n\n  return (\n    <div className={cn(\"flex items-center gap-[3px]\", className)}>\n      <button\n        type=\"button\"\n        onClick={() => setPage(Math.max(1, page - 1))}\n        disabled={page === 1}\n        aria-label=\"Previous page\"\n        className=\"w-8 h-8 rounded-lg border-[1.5px] border-border bg-background text-foreground/70 flex items-center justify-center disabled:opacity-35 disabled:cursor-not-allowed hover:bg-muted transition-colors\"\n      >\n        <ChevronLeft className=\"w-4 h-4\" strokeWidth={2.5} />\n      </button>\n\n      {Array.from({ length: windowSize }).map((_, i) => {\n        const pageNum = start + i;\n        const isActive = page === pageNum;\n        return (\n          <button\n            key={pageNum}\n            type=\"button\"\n            onClick={() => setPage(pageNum)}\n            aria-current={isActive ? \"page\" : undefined}\n            className={cn(\n              \"w-8 h-8 rounded-lg text-[13px] font-medium border-[1.5px] flex items-center justify-center transition-all\",\n              isActive\n                ? cn(theme.bg, theme.border, \"font-semibold\")\n                : \"border-transparent text-foreground/70 hover:bg-muted hover:border-border\",\n            )}\n          >\n            {pageNum}\n          </button>\n        );\n      })}\n\n      <button\n        type=\"button\"\n        onClick={() => setPage(Math.min(totalPages, page + 1))}\n        disabled={page === totalPages}\n        aria-label=\"Next page\"\n        className=\"w-8 h-8 rounded-lg border-[1.5px] border-border bg-background text-foreground/70 flex items-center justify-center disabled:opacity-35 disabled:cursor-not-allowed hover:bg-muted transition-colors\"\n      >\n        <ChevronRight className=\"w-4 h-4\" strokeWidth={2.5} />\n      </button>\n    </div>\n  );\n}\n\n/** `TablePagination` wired to the DataTable's paging. */\nexport function DataTablePagination({ className }: { className?: string }) {\n  const { page, setPage, totalPages } = useDataTable();\n  return (\n    <TablePagination\n      page={page}\n      totalPages={totalPages}\n      onPageChange={setPage}\n      className={className}\n    />\n  );\n}\n\n// ==========================================\n// FILTERS\n// ==========================================\nexport interface DataTableTab {\n  value: string;\n  label: React.ReactNode;\n}\n\nexport interface DataTableTabsProps<T = unknown> {\n  /** The field the tabs filter on: a dot path, or an accessor function (then give `filterId`). */\n  by: Accessor<T>;\n  /** Identifies this filter. Defaults to `by` when that is a string. */\n  filterId?: string;\n  tabs: DataTableTab[];\n  /** The tab that shows every row. */\n  allValue?: string;\n  /** Show how many rows each tab would leave. */\n  showCounts?: boolean;\n  /** Accessible name of the group. */\n  label?: string;\n  size?: \"sm\" | \"md\";\n  className?: string;\n}\n\n/**\n * Pill tabs that switch the table's view (All, Active, Invited…) by filtering\n * one field. Counts respect the search and every other filter.\n */\nfunction DataTableTabsImpl<T>({\n  by,\n  filterId,\n  tabs,\n  allValue = \"all\",\n  showCounts = true,\n  label = \"View\",\n  size = \"sm\",\n  className,\n}: DataTableTabsProps<T>) {\n  const { filters, setFilter, countFor } = useDataTable<T>();\n  const autoId = useId();\n  const id = filterId ?? (typeof by === \"string\" ? by : autoId);\n  const value = filters[id] ?? allValue;\n\n  return (\n    // On narrow screens the tabs scroll sideways rather than wrap.\n    <div\n      className={cn(\n        \"-mx-1 min-w-0 max-w-full px-1 py-0.5 [overflow-x:auto] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n        className,\n      )}\n    >\n      <ToggleGroup\n        type=\"single\"\n        variant=\"segmented\"\n        size={size}\n        value={value}\n        onValueChange={(next) =>\n          setFilter(id, by, !next || next === allValue ? null : next)\n        }\n        aria-label={label}\n      >\n        {tabs.map((tab) => (\n          <ToggleGroupItem key={tab.value} value={tab.value}>\n            {tab.label}\n            {showCounts && (\n              <span className=\"ml-1.5 tabular-nums text-muted-foreground\">\n                {countFor(id, by, tab.value === allValue ? null : tab.value)}\n              </span>\n            )}\n          </ToggleGroupItem>\n        ))}\n      </ToggleGroup>\n    </div>\n  );\n}\n\n/** Generic over the row shape, so a function `by` is typed against your rows. */\nexport const DataTableTabs = DataTableTabsImpl as <T = unknown>(\n  props: DataTableTabsProps<T>,\n) => React.ReactElement | null;\n\nexport interface DataTableFilterOption {\n  label: string;\n  value: string;\n}\n\nexport interface DataTableFilterProps<T = unknown> {\n  /** The field to match: a dot path, or an accessor function (then give `filterId`). */\n  by: Accessor<T>;\n  /** Identifies this filter. Defaults to `by` when that is a string. */\n  filterId?: string;\n  /** Plain strings, or `{ label, value }` when the stored value differs from the label. */\n  options: (string | DataTableFilterOption)[];\n  /** Shown while no value is chosen, e.g. \"Role\". */\n  placeholder: string;\n  /** Accessible name. Defaults to \"Filter by {placeholder}\". */\n  label?: string;\n  side?: \"top\" | \"bottom\";\n  align?: \"start\" | \"end\";\n  className?: string;\n}\n\n/**\n * A single-choice filter. In the `soft` variant it is a pill that takes the\n * accent tint once a value is chosen, with a × to clear it.\n */\nfunction DataTableFilterImpl<T>({\n  by,\n  filterId,\n  options,\n  placeholder,\n  label,\n  side = \"bottom\",\n  align = \"start\",\n  className,\n}: DataTableFilterProps<T>) {\n  const { filters, setFilter } = useDataTable<T>();\n  const { variant, theme } = useTableStyles();\n  const {\n    open: menuOpen,\n    setOpen: setMenuOpen,\n    rootRef: menuRootRef,\n    triggerRef: menuTriggerRef,\n    listRef: menuListRef,\n    close: closeMenu,\n    onListKeyDown: onMenuKeyDown,\n  } = useMenu();\n  const listId = useId();\n  const autoId = useId();\n  const id = filterId ?? (typeof by === \"string\" ? by : autoId);\n  const soft = variant === \"soft\";\n  const accessibleName = label ?? `Filter by ${placeholder.toLowerCase()}`;\n\n  const items = options.map((option) =>\n    typeof option === \"string\" ? { label: option, value: option } : option,\n  );\n  const value = filters[id] ?? null;\n  const current = items.find((item) => item.value === value);\n\n  const choose = (next: string | null) => {\n    setFilter(id, by, next);\n    closeMenu();\n  };\n\n  return (\n    <div\n      ref={menuRootRef}\n      className={cn(\"relative inline-flex min-w-0\", soft && \"flex-1 sm:flex-none\", className)}\n    >\n      <button\n        ref={menuTriggerRef}\n        type=\"button\"\n        onClick={() => setMenuOpen((open) => !open)}\n        aria-haspopup=\"listbox\"\n        aria-expanded={menuOpen}\n        aria-controls={menuOpen ? listId : undefined}\n        aria-label={current ? `${accessibleName}: ${current.label}` : accessibleName}\n        className={cn(\n          soft\n            ? cn(\n                \"flex h-10 w-full min-w-0 items-center justify-between gap-1 rounded-full pl-4 text-sm sm:w-44\",\n                FOCUS_RING,\n                current ? cn(SOFT_PILL_ACTIVE, \"pr-9\") : cn(SOFT_PILL, \"pr-2.5\"),\n              )\n            : cn(\n                \"flex h-9 items-center gap-2 rounded-lg border bg-background pl-3 text-xs font-semibold text-foreground shadow-sm transition-colors\",\n                theme.borderSoft || \"border-border\",\n                theme.hoverBorder,\n                current ? cn(theme.bgSoft, theme.text, \"pr-8\") : \"pr-2.5\",\n              ),\n        )}\n      >\n        <span className=\"truncate\">{current?.label ?? placeholder}</span>\n        {!current && (\n          <ChevronDown\n            aria-hidden=\"true\"\n            className={cn(\n              \"shrink-0 text-muted-foreground transition-transform duration-200\",\n              soft ? \"size-4\" : \"size-3.5\",\n              menuOpen && \"rotate-180\",\n            )}\n          />\n        )}\n      </button>\n      {current && (\n        <button\n          type=\"button\"\n          onClick={() => setFilter(id, by, null)}\n          aria-label={`Clear ${placeholder.toLowerCase()} filter`}\n          className={cn(\n            \"absolute top-1/2 grid size-6 -translate-y-1/2 place-items-center rounded-full text-current transition-colors hover:bg-[color-mix(in_oklab,currentColor_14%,transparent)]\",\n            soft ? \"right-2 text-[var(--tbl-accent-ink)]\" : \"right-1.5\",\n            FOCUS_RING,\n          )}\n        >\n          <X className=\"size-3.5\" aria-hidden=\"true\" />\n        </button>\n      )}\n      {menuOpen && (\n        <div\n          ref={menuListRef}\n          id={listId}\n          role=\"listbox\"\n          aria-label={accessibleName}\n          onKeyDown={onMenuKeyDown}\n          className={cn(\n            soft\n              ? cn(SOFT_MENU, \"max-h-72 [overflow-y:auto]\")\n              : \"absolute z-50 w-max min-w-full rounded-xl border border-border bg-popover py-1 text-popover-foreground shadow-lg\",\n            align === \"end\" ? \"right-0\" : \"left-0\",\n            side === \"top\" ? \"bottom-full mb-2\" : \"top-full mt-2\",\n          )}\n        >\n          {items.map((item) => {\n            const selected = item.value === value;\n            return (\n              <button\n                key={item.value}\n                type=\"button\"\n                role=\"option\"\n                aria-selected={selected}\n                onClick={() => choose(selected ? null : item.value)}\n                className={cn(\n                  soft\n                    ? SOFT_OPTION\n                    : \"flex w-full cursor-pointer items-center justify-between gap-3 px-3 py-1.5 text-left text-xs font-medium text-foreground/80 outline-none hover:bg-muted focus-visible:bg-muted\",\n                  selected &&\n                    (soft ? \"font-medium text-[var(--tbl-accent-ink)]\" : cn(theme.text, \"font-semibold\")),\n                )}\n              >\n                <span className=\"truncate\">{item.label}</span>\n                {selected && (\n                  <Check aria-hidden=\"true\" className={soft ? \"size-4\" : \"size-3.5\"} />\n                )}\n              </button>\n            );\n          })}\n        </div>\n      )}\n    </div>\n  );\n}\n\n/** Generic over the row shape, so a function `by` is typed against your rows. */\nexport const DataTableFilter = DataTableFilterImpl as <T = unknown>(\n  props: DataTableFilterProps<T>,\n) => React.ReactElement | null;\n\n/**\n * Clears the search, every filter and the sort. Renders only while there is\n * something to clear. Children replace the default icon and \"Reset\" label.\n */\nexport const DataTableReset = forwardRef<HTMLButtonElement, TableActionProps>(\n  ({ variant = \"ghost\", onClick, children, ...props }, ref) => {\n    const { isFiltered, sort, resetAll } = useDataTable();\n    if (!isFiltered && !sort) return null;\n    return (\n      <TableAction\n        ref={ref}\n        variant={variant}\n        onClick={(event) => {\n          onClick?.(event);\n          resetAll();\n        }}\n        {...props}\n      >\n        {children ?? (\n          <>\n            <RotateCcw aria-hidden=\"true\" />\n            Reset\n          </>\n        )}\n      </TableAction>\n    );\n  },\n);\nDataTableReset.displayName = \"DataTableReset\";\n",
      "type": "registry:component",
      "target": "components/custom/blocks/TableBlock.tsx"
    },
    {
      "path": "src/components/custom/blocks/table-core.ts",
      "content": "\"use client\";\n\nimport React, { createContext, useContext } from \"react\";\nimport { resolveAccent } from \"@/lib/accent\";\n\n/**\n * `soft` is the admin-dashboard look: a rounded panel with a hairline ring,\n * a tinted header band, pill controls (search, filters, page size, paging)\n * and round row actions, all drawn from the theme colour.\n */\nexport type TableVariant = \"default\" | \"minimal\" | \"glass\" | \"soft\";\n\nexport type TableSize = \"sm\" | \"md\" | \"lg\";\n\nconst colorThemes: Record<string, Record<string, string>> = {\n  primary: {\n    bg: \"bg-primary text-primary-foreground\",\n    text: \"text-primary\",\n    bgSoft: \"bg-primary/10\",\n    border: \"border-primary/30\",\n    borderSoft: \"border-primary/12\",\n    focusRing: \"focus:border-primary/30 focus:outline-none\",\n    hoverBorder: \"hover:border-primary/30\",\n    hoverBgSoft: \"hover:bg-primary/5\",\n  },\n  indigo: {\n    bg: \"bg-indigo-600 text-white\",\n    text: \"text-indigo-600 dark:text-indigo-400\",\n    bgSoft: \"bg-indigo-50 dark:bg-indigo-500/10\",\n    border: \"border-indigo-600\",\n    borderSoft: \"border-indigo-500/15 dark:border-indigo-400/15\",\n    focusRing: \"focus:border-indigo-500 dark:focus:border-indigo-400 focus:outline-none\",\n    hoverBorder: \"hover:border-indigo-500 dark:hover:border-indigo-400\",\n    hoverBgSoft: \"hover:bg-indigo-50/80 dark:hover:bg-indigo-500/10\",\n  },\n  rose: {\n    bg: \"bg-rose-600 text-white\",\n    text: \"text-rose-600 dark:text-rose-400\",\n    bgSoft: \"bg-rose-50 dark:bg-rose-500/10\",\n    border: \"border-rose-600\",\n    borderSoft: \"border-rose-500/15 dark:border-rose-400/15\",\n    focusRing: \"focus:border-rose-500 dark:focus:border-rose-400 focus:outline-none\",\n    hoverBorder: \"hover:border-rose-500 dark:hover:border-rose-400\",\n    hoverBgSoft: \"hover:bg-rose-50/80 dark:hover:bg-rose-500/10\",\n  },\n  emerald: {\n    bg: \"bg-emerald-600 text-white\",\n    text: \"text-emerald-600 dark:text-emerald-400\",\n    bgSoft: \"bg-emerald-50 dark:bg-emerald-500/10\",\n    border: \"border-emerald-600\",\n    borderSoft: \"border-emerald-500/15 dark:border-emerald-400/15\",\n    focusRing: \"focus:border-emerald-500 dark:focus:border-emerald-400 focus:outline-none\",\n    hoverBorder: \"hover:border-emerald-500 dark:hover:border-emerald-400\",\n    hoverBgSoft: \"hover:bg-emerald-50/80 dark:hover:bg-emerald-500/10\",\n  },\n  zinc: {\n    bg: \"bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900\",\n    text: \"text-zinc-900 dark:text-zinc-100\",\n    bgSoft: \"bg-zinc-100 dark:bg-zinc-800/50\",\n    border: \"border-zinc-900 dark:border-zinc-100\",\n    borderSoft: \"border-zinc-300/40 dark:border-zinc-800/60\",\n    focusRing: \"focus:border-zinc-700 dark:focus:border-zinc-300 focus:outline-none\",\n    hoverBorder: \"hover:border-zinc-700 dark:hover:border-zinc-300\",\n    hoverBgSoft: \"hover:bg-zinc-100 dark:hover:bg-zinc-800/60\",\n  },\n};\n\nconst hexToRgb = (hex: string) => {\n  let cleanHex = hex.replace(/^#/, \"\");\n  if (cleanHex.length === 3)\n    cleanHex = cleanHex\n      .split(\"\")\n      .map((x) => x + x)\n      .join(\"\");\n  const result = /^([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(cleanHex);\n  return result\n    ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`\n    : \"99, 102, 241\";\n};\n\nexport const getThemeStyles = (color?: string) => {\n  if (!color || color === \"default\" || color === \"primary\") {\n    return { theme: colorThemes.primary, style: {} };\n  }\n  const resolved = resolveAccent(color);\n  const target = resolved || color;\n  if (target && target.startsWith(\"#\")) {\n    const rgb = hexToRgb(target);\n    return {\n      theme: {\n        bg: \"bg-[var(--theme-color)] text-white\",\n        text: \"text-[var(--theme-color)]\",\n        bgSoft: \"bg-[rgba(var(--theme-color-rgb),0.1)]\",\n        border: \"border-[var(--theme-color)]\",\n        borderSoft: \"border-[rgba(var(--theme-color-rgb),0.18)]\",\n        focusRing: \"focus:border-[var(--theme-color)] focus:outline-none\",\n        hoverBorder: \"hover:border-[var(--theme-color)]\",\n        hoverBgSoft: \"hover:bg-[rgba(var(--theme-color-rgb),0.08)]\",\n      },\n      style: {\n        \"--theme-color\": target,\n        \"--theme-color-rgb\": rgb,\n      } as React.CSSProperties,\n    };\n  }\n  return { theme: colorThemes[color] || colorThemes.primary, style: {} };\n};\n\nconst accentHex: Record<string, string> = {\n  lavender: \"#9b8afb\",\n  indigo: \"#6366f1\",\n  rose: \"#f43f5e\",\n  emerald: \"#10b981\",\n  zinc: \"#3f3f46\",\n};\n\n/** The table colour as a CSS colour, for the tints the `soft` variant mixes from it. */\nexport const getAccentColor = (color?: string) => {\n  if (!color || color === \"default\" || color === \"primary\") return \"var(--primary)\";\n  const resolved = resolveAccent(color);\n  if (resolved) return resolved;\n  if (color.startsWith(\"#\") || color.startsWith(\"var(\")) return color;\n  return accentHex[color] ?? \"var(--primary)\";\n};\n\nexport interface TableStyleContext {\n  variant: TableVariant;\n  size: TableSize;\n  theme: Record<string, string>;\n}\n\nexport const TableContext = createContext<TableStyleContext>({\n  variant: \"default\",\n  size: \"md\",\n  theme: colorThemes.primary,\n});\n\nexport const useTableStyles = () => useContext(TableContext);\n\nexport type Accessor<T> = string | ((row: T) => unknown);\n\nexport type SortType = \"auto\" | \"string\" | \"number\" | \"date\";\n\nexport function resolveAccessor<T>(row: T, accessor: Accessor<T>): unknown {\n  if (typeof accessor === \"function\") return accessor(row);\n  return accessor\n    .split(\".\")\n    .reduce<unknown>(\n      (acc, key) =>\n        acc == null ? acc : (acc as Record<string, unknown>)[key],\n      row,\n    );\n}\n\nexport interface DataTableContextValue<T = unknown> {\n  /** Exactly what you passed in. */\n  sourceRows: T[];\n  /** After search + sort, before paging. */\n  filteredRows: T[];\n  /** The current page — what you render. */\n  rows: T[];\n  getRowId: (row: T) => string | number;\n\n  search: string;\n  setSearch: (value: string) => void;\n\n  /** Active filters by id, as set by DataTableFilter and DataTableTabs. */\n  filters: Record<string, string>;\n  /** `null` clears the filter. Rows match when the accessor's value equals `value`. */\n  setFilter: (id: string, accessor: Accessor<T>, value: string | null) => void;\n  /**\n   * How many rows a filter value would leave: search and every other filter\n   * apply, this one is swapped for `value` (`null` for \"any\").\n   */\n  countFor: (id: string, accessor: Accessor<T>, value: string | null) => number;\n  /** Search or any filter is narrowing the rows. */\n  isFiltered: boolean;\n  /** Clears search, every filter and the sort. */\n  resetAll: () => void;\n\n  sort: { id: string; direction: \"asc\" | \"desc\" } | null;\n  toggleSort: (id: string, accessor: Accessor<T>, type: SortType) => void;\n\n  page: number;\n  setPage: (page: number) => void;\n  pageSize: number;\n  setPageSize: (size: number) => void;\n  pageSizeOptions: number[];\n  totalPages: number;\n  total: number;\n  from: number;\n  to: number;\n\n  selectedIds: Set<string | number>;\n  selectedRows: T[];\n  isSelected: (id: string | number) => boolean;\n  toggleRow: (id: string | number) => void;\n  toggleAllOnPage: () => void;\n  clearSelection: () => void;\n  isAllPageSelected: boolean;\n  isSomePageSelected: boolean;\n\n  isExpanded: (id: string | number) => boolean;\n  toggleExpanded: (id: string | number) => void;\n\n  theme: Record<string, string>;\n  themeStyle?: React.CSSProperties;\n}\n\nexport const DataTableContext = createContext<DataTableContextValue | null>(null);\n\nexport function useDataTable<T = unknown>(): DataTableContextValue<T> {\n  const ctx = useContext(DataTableContext);\n  if (!ctx) {\n    throw new Error(\"useDataTable must be used inside <DataTable>.\");\n  }\n  return ctx as DataTableContextValue<T>;\n}\n",
      "type": "registry:component",
      "target": "components/custom/blocks/table-core.ts"
    },
    {
      "path": "src/lib/accent.ts",
      "content": "import type { CSSProperties } from \"react\";\n\n/**\n * Bridging the docs colour picker into a preview.\n *\n * Most blocks are already written against the `--primary` token (`bg-primary`,\n * `ring-primary/30`, `text-primary-foreground`…). Rather than teaching each one a\n * bespoke `color` prop, a demo can repoint that token for its own subtree and\n * every one of those classes follows along.\n */\n\n/** The swatches the picker offers, as concrete CSS colours. */\nconst PRESET_HEX: Record<string, string> = {\n  lavender: \"#9b8afb\",\n  indigo: \"#6366f1\",\n  rose: \"#f43f5e\",\n  emerald: \"#10b981\",\n};\n\n/** True when the picker sits on \"Default\" — the page theme should win. */\nexport function isDefaultAccent(color?: string): boolean {\n  return !color || color === \"primary\" || color === \"default\";\n}\n\n/**\n * Turns a picker value into a CSS colour, or null for \"Default\" — meaning\n * nothing should be overridden, so the theme (and the sidebar customizer)\n * keeps control.\n */\nexport function resolveAccent(color?: string): string | null {\n  if (isDefaultAccent(color)) return null;\n  return PRESET_HEX[color as string] ?? (color as string);\n}\n\n/**\n * The picker value in a form every block accepts. Blocks know the older swatch\n * names but not \"lavender\" — and since that is also a CSS named colour\n * (#e6e6fa), a block that passes unknown names straight through would paint\n * the wrong shade, while others fall back to a different palette. Demos that\n * hand the picker value to such a block run it through this first. Every\n * other value is returned untouched.\n */\nexport function toBlockColor(color: string): string;\nexport function toBlockColor(color: string | undefined): string | undefined;\nexport function toBlockColor(color?: string): string | undefined {\n  return color === \"lavender\" ? PRESET_HEX.lavender : color;\n}\n\nfunction parseHex(input: string): [number, number, number] | null {\n  const hex = input.trim().replace(/^#/, \"\");\n  if (/^[a-f\\d]{3}$/i.test(hex)) {\n    const [r, g, b] = hex.split(\"\");\n    return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16)];\n  }\n  if (/^[a-f\\d]{6}$/i.test(hex)) {\n    return [\n      parseInt(hex.slice(0, 2), 16),\n      parseInt(hex.slice(2, 4), 16),\n      parseInt(hex.slice(4, 6), 16),\n    ];\n  }\n  return null;\n}\n\n/** Legible foreground for content sitting on a filled `color` swatch. */\nexport function accentForeground(color: string): string {\n  const rgb = parseHex(color);\n  if (!rgb) return \"#ffffff\";\n  const channel = (v: number) => {\n    const c = v / 255;\n    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n  };\n  const luminance =\n    0.2126 * channel(rgb[0]) +\n    0.7152 * channel(rgb[1]) +\n    0.0722 * channel(rgb[2]);\n  return luminance > 0.45 ? \"#09090b\" : \"#ffffff\";\n}\n\n/** The theme tokens a picked accent repoints, as `--name` → value. */\nexport function accentTokens(color?: string): Record<string, string> {\n  const resolved = resolveAccent(color);\n  if (!resolved) return {};\n  return {\n    \"--primary\": resolved,\n    \"--primary-foreground\": accentForeground(resolved),\n    \"--ring\": resolved,\n  };\n}\n\n/**\n * Inline style repointing `--primary` at the picked accent for one subtree.\n * `--primary-foreground` is derived alongside it, otherwise a light pick leaves\n * `text-primary-foreground` content unreadable on its own fill. Returns `{}` on\n * \"Default\" so the page theme still wins.\n */\nexport function accentStyle(color?: string): CSSProperties {\n  return accentTokens(color) as CSSProperties;\n}\n",
      "type": "registry:component",
      "target": "lib/accent.ts"
    }
  ],
  "docs": "Named exports: Table, TableHeading, TableContent, TableHeader, TableBody, TableRow, TableHead, TableCell, TableFooter, TableEmpty, TableExpandedRow, TableExpandToggle, TableRowActions, useTableStyles, getThemeStyles. Each primitive renders its native element and forwards refs, so `colSpan` and aria attributes pass through. Variant (default | minimal | glass | soft), size (sm | md | lg) and color are set once on `Table` and read from context; `soft` is the admin-dashboard look (rounded panel, tinted header band, pill controls, round row actions). Also exports the stateful DataTable family — DataTableTabs, DataTableFilter, DataTableReset, DataTableBatchAction — and the cell/toolbar parts TableAction, TableRowAction, TableBadge and TablePerson. `sticky` on TableHead/TableCell pins a column to the right edge. Sorting and expansion are controlled props — the component never reorders your data. For search, paging, selection and batch actions wired up for you, use `advanced-table`.",
  "categories": [
    "data-display"
  ],
  "type": "registry:component"
}
