{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table",
  "title": "Table",
  "description": "A semantic table in a scroll container, with sortable headers, loading, empty and error rows.",
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/theme.json",
    "https://sproutboat.com/ikada/r/cn.json",
    "https://sproutboat.com/ikada/r/icons.json",
    "https://sproutboat.com/ikada/r/skeleton.json"
  ],
  "files": [
    {
      "path": "registry/ikada/ui/table.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { cn, focusRing, focusRingInset } from \"@/components/ikada/lib/cn\";\nimport { AlertIcon, ArrowDownIcon, ArrowUpIcon, SortIcon } from \"@/components/ikada/lib/icons\";\nimport { Skeleton } from \"@/components/ikada/skeleton\";\n\nexport type SortDirection = \"asc\" | \"desc\";\n\n/** Cycles none, ascending, descending, none. */\nexport function nextSort(current: SortDirection | undefined): SortDirection | undefined {\n  return current === undefined ? \"asc\" : current === \"asc\" ? \"desc\" : undefined;\n}\n\n/**\n * Container-query breakpoints for `hideBelow`. They measure the table's own scroll\n * container, not the viewport: sm 24rem, md 28rem, lg 32rem, xl 36rem, 2xl 42rem, 3xl 48rem, 4xl 56rem.\n * Full class names so Tailwind can see them.\n */\nexport type TableBreakpoint = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"3xl\" | \"4xl\";\n\nconst hideBelowClass = {\n  sm: \"@max-sm:hidden\",\n  md: \"@max-md:hidden\",\n  lg: \"@max-lg:hidden\",\n  xl: \"@max-xl:hidden\",\n  \"2xl\": \"@max-2xl:hidden\",\n  \"3xl\": \"@max-3xl:hidden\",\n  \"4xl\": \"@max-4xl:hidden\",\n} satisfies Record<TableBreakpoint, string>;\n\nexport interface HideBelowProps {\n  /**\n   * Drops this column (set it on its `TableHead` and every `TableCell`) when the table's\n   * container is narrower than the breakpoint. Use it for low-priority columns only; never put critical\n   * status (failure) in a column that can hide; the\n   * container still scrolls sideways as the last resort.\n   */\n  hideBelow?: TableBreakpoint;\n}\n\nexport interface TableProps extends ComponentProps<\"table\"> {\n  /** Names the scrolling region for screen readers. Required: say what the table lists. */\n  label: string;\n  /** Header stays put while the body scrolls. Pair with `maxHeight`. */\n  stickyHeader?: boolean;\n  /** CSS max-height of the scroll area, e.g. \"24rem\". */\n  maxHeight?: string;\n  /** Applied to the scroll container. */\n  containerClassName?: string;\n}\n\n/**\n * A semantic table in a scroll container. Wide tables scroll inside it, never the page;\n * the container is focusable so a keyboard can scroll it. 48px rows (36px compact) are\n * set by `--sb-row-h`.\n */\nexport function Table({\n  label,\n  stickyHeader = false,\n  maxHeight,\n  containerClassName,\n  className,\n  children,\n  ref,\n  ...props\n}: TableProps) {\n  return (\n    <div\n      data-slot=\"table\"\n      role=\"region\"\n      aria-label={label}\n      // oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- a scroll region must be keyboard-focusable\n      tabIndex={0}\n      data-sticky={stickyHeader || undefined}\n      style={maxHeight ? { maxHeight } : undefined}\n      className={cn(\n        \"@container min-w-0 max-w-full overflow-auto overscroll-x-contain rounded-xl bg-surface shadow-card\",\n        focusRingInset,\n        containerClassName,\n      )}\n    >\n      <table\n        ref={ref}\n        className={cn(\"w-full border-separate border-spacing-0 text-start text-sb-base\", className)}\n        {...props}\n      >\n        {children}\n      </table>\n    </div>\n  );\n}\n\nexport function TableHeader({ className, ...props }: ComponentProps<\"thead\">) {\n  return (\n    <thead\n      data-slot=\"table-header\"\n      className={cn(\n        \"[[data-sticky]_&_th]:sticky [[data-sticky]_&_th]:top-0 [[data-sticky]_&_th]:z-(--sb-z-sticky)\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function TableBody({ className, ...props }: ComponentProps<\"tbody\">) {\n  return <tbody data-slot=\"table-body\" className={className} {...props} />;\n}\n\nexport interface TableRowProps extends ComponentProps<\"tr\"> {\n  /** Highlights the row and sets aria-selected. Pair with a checkbox cell: that is the control. */\n  selected?: boolean;\n}\n\nexport function TableRow({ selected = false, className, ...props }: TableRowProps) {\n  return (\n    <tr\n      data-slot=\"table-row\"\n      aria-selected={selected || undefined}\n      data-selected={selected || undefined}\n      className={cn(\n        \"group/row transition-colors duration-(--sb-duration-hover) ease-hover hover:bg-hover/60\",\n        \"data-selected:bg-hover data-selected:hover:bg-hover\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst cellBase = \"px-4 text-start align-middle\";\n\nexport interface NumericProps {\n  /** Right-aligns the column (start edge in RTL reads end) and sets tabular figures. Set it on the `TableHead` and every `TableCell` of a number column. */\n  numeric?: boolean;\n}\n\nconst numericClass = \"text-end tabular-nums\";\n\nexport interface TableHeadProps\n  extends Omit<ComponentProps<\"th\">, \"onClick\" | \"aria-sort\">, HideBelowProps, NumericProps {\n  /** Makes the header a button. `undefined` means sortable but not sorted. */\n  sortable?: boolean;\n  /** Current sort of this column; shows the arrow and sets aria-sort. */\n  sort?: SortDirection;\n  /** Makes the header a sort button; called when it is pressed. */\n  onSort?: () => void;\n}\n\n/** A column header. Sortable ones hold a button (Enter or Space) and set `aria-sort`. */\nexport function TableHead({\n  sortable = false,\n  sort,\n  onSort,\n  hideBelow,\n  numeric,\n  className,\n  children,\n  ...props\n}: TableHeadProps) {\n  const ariaSort = sortable ? (sort === \"asc\" ? \"ascending\" : sort === \"desc\" ? \"descending\" : \"none\") : undefined;\n  return (\n    <th\n      data-slot=\"table-head\"\n      scope=\"col\"\n      aria-sort={ariaSort}\n      className={cn(\n        cellBase,\n        \"h-10 whitespace-nowrap bg-surface text-sb-sm font-semibold text-fg-muted shadow-[inset_0_-1px_0_var(--sb-hairline)]\",\n        numeric && numericClass,\n        hideBelow && hideBelowClass[hideBelow],\n        className,\n      )}\n      {...props}\n    >\n      {sortable ? (\n        <button\n          type=\"button\"\n          onClick={onSort}\n          className={cn(\n            \"-mx-2 inline-flex h-8 items-center gap-1.5 rounded-md px-2 font-semibold transition-colors duration-(--sb-duration-hover) ease-hover hover:bg-hover hover:text-fg pointer-coarse:min-h-11\",\n            sort && \"text-fg\",\n            focusRing,\n          )}\n        >\n          {children}\n          {sort === \"asc\" ? (\n            <ArrowUpIcon className=\"size-3.5\" />\n          ) : sort === \"desc\" ? (\n            <ArrowDownIcon className=\"size-3.5\" />\n          ) : (\n            <SortIcon className=\"size-3.5 opacity-60\" />\n          )}\n        </button>\n      ) : (\n        children\n      )}\n    </th>\n  );\n}\n\nexport function TableCell({\n  hideBelow,\n  numeric,\n  className,\n  ...props\n}: ComponentProps<\"td\"> & HideBelowProps & NumericProps) {\n  return (\n    <td\n      data-slot=\"table-cell\"\n      className={cn(\n        cellBase,\n        \"h-(--sb-row-h) shadow-[inset_0_-1px_0_var(--sb-hairline)] group-last/row:shadow-none\",\n        numeric && numericClass,\n        hideBelow && hideBelowClass[hideBelow],\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\n/** One cell spanning every column, for the empty, loading and error bodies. */\nfunction StateRow({ colSpan, className, children }: { colSpan: number; className?: string; children: ReactNode }) {\n  return (\n    <TableRow className=\"hover:bg-transparent\">\n      <td data-slot=\"table-state\" colSpan={colSpan} className={cn(\"px-4 py-10 text-center\", className)}>\n        {children}\n      </td>\n    </TableRow>\n  );\n}\n\nexport function TableEmpty({ colSpan, children }: { colSpan: number; children: ReactNode }) {\n  return <StateRow colSpan={colSpan}>{children}</StateRow>;\n}\n\nexport function TableError({\n  colSpan,\n  title = \"Could not load this list\",\n  children,\n  action,\n}: {\n  colSpan: number;\n  title?: string;\n  children?: ReactNode;\n  /** A retry Button. */\n  action?: ReactNode;\n}) {\n  return (\n    <StateRow colSpan={colSpan}>\n      <div role=\"alert\" className=\"mx-auto flex max-w-sm flex-col items-center gap-2\">\n        <AlertIcon className=\"size-5 text-danger-fg\" />\n        <p className=\"font-semibold\">{title}</p>\n        {children ? <p className=\"text-fg-muted\">{children}</p> : null}\n        {action ? <div className=\"mt-1\">{action}</div> : null}\n      </div>\n    </StateRow>\n  );\n}\n\n/** Skeleton rows that keep the layout of the loaded table. Set `aria-busy` on the Table while loading. */\nexport function TableLoading({ columns, rows = 4 }: { columns: number; rows?: number }) {\n  return Array.from({ length: rows }, (_, r) => (\n    <TableRow key={r} className=\"hover:bg-transparent\" aria-hidden=\"true\">\n      {Array.from({ length: columns }, (_, c) => (\n        <TableCell key={c}>\n          <Skeleton className={c === 0 ? \"w-24\" : c % 2 ? \"w-3/4\" : \"w-1/2\"} />\n        </TableCell>\n      ))}\n    </TableRow>\n  ));\n}\n",
      "type": "registry:ui",
      "target": "@components/ikada/table.tsx"
    }
  ],
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}