{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"table","title":"Table","description":"React Aria table with composable rows and cells plus optional sorting, row or checkbox selection, captions and footers.","dependencies":["lucide-react@1.18.0","react-aria-components@1.20.0"],"registryDependencies":["@siteplane/base"],"files":[{"path":"packages/ui/src/components/table/table.tsx","content":"\"use client\";\n\nimport {\n  ChevronDownIcon,\n  ChevronsUpDownIcon,\n  ChevronUpIcon,\n} from \"lucide-react\";\nimport {\n  Cell as AriaCell,\n  Checkbox as AriaCheckbox,\n  CollectionRendererContext,\n  Column as AriaColumn,\n  composeRenderProps,\n  Row as AriaRow,\n  Table as AriaTable,\n  TableBody as AriaTableBody,\n  TableFooter as AriaTableFooter,\n  TableHeader as AriaTableHeader,\n  TableStateContext,\n  type CellProps as AriaCellProps,\n  type CheckboxProps as AriaCheckboxProps,\n  type ColumnProps as AriaColumnProps,\n  type Key,\n  type PressEvent,\n  type RowProps as AriaRowProps,\n  type Selection as AriaSelection,\n  type SortDescriptor as AriaSortDescriptor,\n  type TableBodyProps as AriaTableBodyProps,\n  type TableFooterProps as AriaTableFooterProps,\n  type TableHeaderProps as AriaTableHeaderProps,\n  type TableProps as AriaTableProps,\n} from \"react-aria-components\";\nimport {\n  Children,\n  type ComponentProps,\n  createContext,\n  isValidElement,\n  type KeyboardEvent,\n  type PointerEvent,\n  type ReactElement,\n  type ReactNode,\n  type SyntheticEvent,\n  useContext,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { CheckboxGlyph } from \"../../system/styles/checkbox-glyph\";\nimport { cn } from \"../../system/utils/cn\";\nimport {\n  tableAlignmentClassName,\n  tableContainerSizeClassName,\n  tableFooterClassName,\n  type TableAlign,\n} from \"./table-shared\";\n\nimport \"./table.css\";\n\nexport type { SortDescriptor } from \"react-aria-components\";\n\nexport type { TableAlign };\nexport type TableSelectionTrigger = \"checkbox\" | \"row\";\n\n/** The table's resolved size, which its selection checkboxes take as theirs. */\nconst TableSizeContext = createContext<SiteplaneControlSize | undefined>(\n  undefined,\n);\n\n/**\n * What a row needs from its table to run its action under\n * `selectionTrigger=\"checkbox\"`: the root `onRowAction`, and a count of the\n * row actions React Aria ran, so a row tells whether a press ran one.\n */\ntype TableRowActions = {\n  checkboxTrigger: boolean;\n  onRowAction: ((key: Key) => void) | undefined;\n  ran: { current: number };\n};\n\nconst TableRowActionsContext = createContext<TableRowActions>({\n  checkboxTrigger: false,\n  onRowAction: undefined,\n  ran: { current: 0 },\n});\n\nexport interface TableProps extends Omit<\n  AriaTableProps,\n  \"className\" | \"sortDescriptor\"\n> {\n  className?: string | undefined;\n  /**\n   * Classes for the scrolling container. The heading row sticks to the top of\n   * this container, so it stays in view only when the container has a height\n   * limit, such as `max-h-80`.\n   */\n  containerClassName?: string | undefined;\n  hoverable?: boolean | undefined;\n  selectionTrigger?: TableSelectionTrigger | undefined;\n  size?: SiteplaneControlSize | undefined;\n  sortDescriptor?: AriaSortDescriptor | undefined;\n}\n\nexport interface TableCaptionProps {\n  children: ReactNode;\n  className?: string;\n}\n\nexport type TableHeaderProps<T> = Omit<AriaTableHeaderProps<T>, \"className\"> & {\n  className?: string;\n};\n\nexport interface TableHeadProps extends Omit<AriaColumnProps, \"className\"> {\n  align?: TableAlign;\n  className?: string;\n}\n\nexport type TableBodyProps<T> = Omit<AriaTableBodyProps<T>, \"className\"> & {\n  className?: string;\n};\n\nexport type TableFooterProps<T> = Omit<AriaTableFooterProps<T>, \"className\"> & {\n  className?: string;\n};\n\nexport interface TableRowProps<T> extends Omit<AriaRowProps<T>, \"className\"> {\n  className?: string | undefined;\n  hoverable?: boolean | undefined;\n  inactive?: boolean | undefined;\n}\n\nexport interface TableCellProps extends Omit<AriaCellProps, \"className\"> {\n  align?: TableAlign;\n  className?: string;\n}\n\nexport interface TableSelectionCheckboxProps extends Omit<\n  AriaCheckboxProps,\n  \"children\" | \"className\" | \"slot\"\n> {\n  className?: string;\n}\n\nfunction isTableCaption(\n  child: ReactNode,\n): child is ReactElement<TableCaptionProps> {\n  return isValidElement(child) && child.type === TableCaption;\n}\n\nexport function Table({\n  \"aria-labelledby\": ariaLabelledBy,\n  children,\n  className,\n  containerClassName,\n  defaultSelectedKeys,\n  hoverable = true,\n  onRowAction,\n  onSelectionChange,\n  selectedKeys,\n  selectionBehavior,\n  selectionTrigger = \"row\",\n  size,\n  sortDescriptor,\n  ...props\n}: TableProps): ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n  const checkboxTrigger = selectionTrigger === \"checkbox\";\n  const actionsRan = useRef(0);\n  const rowActions = useMemo<TableRowActions>(\n    () => ({ checkboxTrigger, onRowAction, ran: actionsRan }),\n    [checkboxTrigger, onRowAction],\n  );\n  const generatedCaptionId = useId();\n  const childArray = Children.toArray(children);\n  const caption = childArray.find(isTableCaption);\n  const tableChildren = childArray.filter((child) => !isTableCaption(child));\n  const captionId = caption ? generatedCaptionId : undefined;\n  const selectionOriginRef = useRef<TableSelectionTrigger>(\"row\");\n  const [internalSelectedKeys, setInternalSelectedKeys] =\n    useState<AriaSelection>(() =>\n      defaultSelectedKeys === \"all\"\n        ? \"all\"\n        : new Set(defaultSelectedKeys ?? []),\n    );\n  const markSelectionOrigin = (target: EventTarget | null) => {\n    selectionOriginRef.current =\n      target instanceof Element &&\n      target.closest('[data-slot=\"table-selection-checkbox\"]')\n        ? \"checkbox\"\n        : \"row\";\n  };\n  const handleSelectionChange = (keys: AriaSelection) => {\n    if (\n      selectionTrigger === \"checkbox\" &&\n      selectionOriginRef.current !== \"checkbox\"\n    ) {\n      return;\n    }\n\n    if (selectedKeys === undefined) setInternalSelectedKeys(keys);\n    onSelectionChange?.(keys);\n  };\n  const labelledBy = ariaLabelledBy ?? captionId;\n\n  return (\n    <div\n      className={cn(\n        \"sp-table-container\",\n        \"relative w-full overflow-x-auto data-[size=lg]:[--table-row-height:--spacing(10)] data-[size=xl]:[--table-row-height:--spacing(12)]\",\n        tableContainerSizeClassName,\n        containerClassName,\n      )}\n      data-size={resolvedSize}\n      data-slot=\"table-container\"\n      onClickCapture={(event) => markSelectionOrigin(event.target)}\n      onKeyDownCapture={(event: KeyboardEvent<HTMLDivElement>) =>\n        markSelectionOrigin(event.target)\n      }\n      onPointerDownCapture={(event: PointerEvent<HTMLDivElement>) =>\n        markSelectionOrigin(event.target)\n      }\n    >\n      <TableSizeContext.Provider value={resolvedSize}>\n        <TableRowActionsContext.Provider value={rowActions}>\n          <AriaTable\n            {...props}\n            {...(labelledBy === undefined\n              ? {}\n              : { \"aria-labelledby\": labelledBy })}\n            {...(onRowAction === undefined\n              ? {}\n              : {\n                  onRowAction: (key: Key) => {\n                    actionsRan.current += 1;\n                    onRowAction(key);\n                  },\n                })}\n            // Only the checkboxes select, so a press on a row never selects\n            // it and there is no double click that runs the action instead.\n            {...(checkboxTrigger\n              ? { selectionBehavior: \"toggle\" as const }\n              : selectionBehavior === undefined\n                ? {}\n                : { selectionBehavior })}\n            className={cn(\n              \"sp-table\",\n              \"w-full caption-bottom text-sm\",\n              className,\n            )}\n            data-hoverable={hoverable}\n            data-selection-trigger={selectionTrigger}\n            data-slot=\"table\"\n            onSelectionChange={handleSelectionChange}\n            selectedKeys={selectedKeys ?? internalSelectedKeys}\n            {...(sortDescriptor === undefined ? {} : { sortDescriptor })}\n          >\n            {tableChildren}\n          </AriaTable>\n        </TableRowActionsContext.Provider>\n      </TableSizeContext.Provider>\n      {caption ? (\n        <div\n          className={cn(\n            \"sp-table-caption\",\n            \"mt-4 text-muted-foreground text-sm\",\n            caption.props.className,\n          )}\n          data-slot=\"table-caption\"\n          id={captionId}\n        >\n          {caption.props.children}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport function TableCaption(props: TableCaptionProps): null {\n  void props;\n  return null;\n}\n\nexport function TableHeader<T>({\n  className,\n  ...props\n}: TableHeaderProps<T>): ReactElement {\n  return (\n    <AriaTableHeader\n      {...props}\n      className={composeRenderProps(className, (value) =>\n        cn(\"sp-table-header\", value),\n      )}\n      data-slot=\"table-header\"\n    />\n  );\n}\n\nexport function TableHead({\n  align = \"start\",\n  allowsSorting,\n  children,\n  className,\n  ...props\n}: TableHeadProps): ReactElement {\n  return (\n    <AriaColumn\n      {...props}\n      {...(allowsSorting === undefined ? {} : { allowsSorting })}\n      className={composeRenderProps(className, (value) =>\n        cn(\n          \"sp-table-head\",\n          \"whitespace-nowrap align-middle\",\n          tableAlignmentClassName(align),\n          value,\n        ),\n      )}\n      data-align={align}\n      data-slot=\"table-head\"\n    >\n      {(renderProps) => (\n        <span\n          className={cn(\n            \"sp-table-head-content\",\n            \"inline-flex items-center\",\n            align === \"center\" && \"justify-center\",\n            align === \"end\" && \"justify-end\",\n          )}\n          data-slot=\"table-head-content\"\n        >\n          {typeof children === \"function\" ? children(renderProps) : children}\n          {allowsSorting ? (\n            renderProps.sortDirection === \"ascending\" ? (\n              <ChevronUpIcon aria-hidden=\"true\" data-default-size=\"\" />\n            ) : renderProps.sortDirection === \"descending\" ? (\n              <ChevronDownIcon aria-hidden=\"true\" data-default-size=\"\" />\n            ) : (\n              <ChevronsUpDownIcon aria-hidden=\"true\" data-default-size=\"\" />\n            )\n          ) : null}\n        </span>\n      )}\n    </AriaColumn>\n  );\n}\n\nexport function TableBody<T>({\n  className,\n  ...props\n}: TableBodyProps<T>): ReactElement {\n  return (\n    <AriaTableBody\n      {...props}\n      className={composeRenderProps(className, (value) =>\n        cn(\"sp-table-body\", value),\n      )}\n      data-slot=\"table-body\"\n    />\n  );\n}\n\nexport function TableFooter<T>({\n  className,\n  ...props\n}: TableFooterProps<T>): ReactElement {\n  return (\n    <AriaTableFooter\n      {...props}\n      className={cn(\"sp-table-footer\", tableFooterClassName, className)}\n      data-slot=\"table-footer\"\n    />\n  );\n}\n\n/**\n * A row of the table. Under `selectionTrigger=\"checkbox\"` only the checkboxes\n * select, so a press on the row runs its action, also while rows are\n * selected: React Aria still takes such a press as a selection toggle, which\n * the table drops, and the row then runs the action itself when the press\n * ends. Space stays the selection key and runs no action; a press that React\n * Aria already ran an action for runs none again.\n */\nexport function TableRow<T>({\n  className,\n  hoverable = true,\n  inactive = false,\n  onAction,\n  onPress,\n  onPressStart,\n  ...props\n}: TableRowProps<T>): ReactElement {\n  const interactive = Boolean(props.href || onAction);\n  const actions = useContext(TableRowActionsContext);\n  const actionsAtPressStart = useRef(0);\n  const { id } = props;\n  const runsOwnAction =\n    actions.checkboxTrigger &&\n    (onAction !== undefined || actions.onRowAction !== undefined);\n  const pressProps = runsOwnAction\n    ? {\n        ...(onAction === undefined\n          ? {}\n          : {\n              onAction: () => {\n                actions.ran.current += 1;\n                onAction();\n              },\n            }),\n        onPress: (event: PressEvent) => {\n          onPress?.(event);\n          if (actions.ran.current !== actionsAtPressStart.current) return;\n          if (event.pointerType === \"keyboard\" && event.key !== \"Enter\") return;\n          onAction?.();\n          if (id !== undefined) actions.onRowAction?.(id);\n        },\n        onPressStart: (event: PressEvent) => {\n          actionsAtPressStart.current = actions.ran.current;\n          onPressStart?.(event);\n        },\n      }\n    : {\n        ...(onAction === undefined ? {} : { onAction }),\n        ...(onPress === undefined ? {} : { onPress }),\n        ...(onPressStart === undefined ? {} : { onPressStart }),\n      };\n\n  return (\n    <AriaRow\n      {...props}\n      {...pressProps}\n      className={composeRenderProps(className, (value) =>\n        cn(\"sp-table-row\", \"relative outline-none\", value),\n      )}\n      data-hoverable={hoverable}\n      data-inactive={inactive || undefined}\n      data-interactive={interactive || undefined}\n      data-slot=\"table-row\"\n    />\n  );\n}\n\n// What a press or a key in a cell can start on besides the cell itself: an\n// element the browser activates or focuses, a label that passes a click on to\n// its control, and an item of another React Aria collection, such as a tag.\nconst cellControlSelector = [\n  \"a[href]\",\n  \"area[href]\",\n  \"audio[controls]\",\n  \"button\",\n  \"embed\",\n  \"iframe\",\n  \"input\",\n  \"label\",\n  \"object\",\n  \"select\",\n  \"summary\",\n  \"textarea\",\n  \"video[controls]\",\n  \"[contenteditable]:not([contenteditable='false'])\",\n  \"[data-collection]\",\n  \"[tabindex]\",\n].join(\", \");\n\n/** Whether the event started on a control inside the cell, not the cell. */\nfunction startsOnCellControl(event: SyntheticEvent): boolean {\n  const cell = event.currentTarget;\n  const control =\n    event.target instanceof Element\n      ? event.target.closest(cellControlSelector)\n      : null;\n\n  return control !== null && control !== cell && cell.contains(control);\n}\n\n/**\n * React Aria's own controls stop their presses from reaching the row, and the\n * row ignores a press that starts on an element it can focus. A control that\n * is not React Aria's, such as a Menu trigger, gets the same: a press or a\n * click that starts in it, also on its icon, and Enter or Space while it has\n * focus stay with it, so they neither run the row's action nor change the\n * selection. Events from a popup of the control reach the cell through\n * React's tree but not through the document, and pass.\n */\nfunction keepPressInControl(event: SyntheticEvent): void {\n  if (startsOnCellControl(event)) event.stopPropagation();\n}\n\nfunction keepKeyInControl(event: KeyboardEvent): void {\n  if (\n    (event.key === \"Enter\" || event.key === \" \") &&\n    startsOnCellControl(event)\n  )\n    event.stopPropagation();\n}\n\nfunction chain<E>(\n  own: (event: E) => void,\n  given: ((event: E) => void) | undefined,\n): (event: E) => void {\n  return (event) => {\n    given?.(event);\n    own(event);\n  };\n}\n\nexport function TableCell({\n  align = \"start\",\n  className,\n  render,\n  ...props\n}: TableCellProps): ReactElement {\n  // A virtualized table renders its cells as divs.\n  const { isVirtualized } = useContext(CollectionRendererContext);\n\n  return (\n    <AriaCell\n      {...props}\n      className={composeRenderProps(className, (value) =>\n        cn(\n          \"sp-table-cell\",\n          \"whitespace-nowrap bg-clip-padding align-middle outline-none\",\n          tableAlignmentClassName(align),\n          value,\n        ),\n      )}\n      data-align={align}\n      data-slot=\"table-cell\"\n      // React Aria takes no key handlers on a cell, so the cell element is\n      // rendered here, with the handlers React Aria gives it kept.\n      render={(elementProps, renderProps) => {\n        const cellProps = elementProps as ComponentProps<\"td\">;\n        const guarded: ComponentProps<\"td\"> = {\n          ...cellProps,\n          onClick: chain(keepPressInControl, cellProps.onClick),\n          onDoubleClick: chain(keepPressInControl, cellProps.onDoubleClick),\n          onKeyDown: chain(keepKeyInControl, cellProps.onKeyDown),\n          onMouseDown: chain(keepPressInControl, cellProps.onMouseDown),\n          onPointerDown: chain(keepPressInControl, cellProps.onPointerDown),\n          onPointerUp: chain(keepPressInControl, cellProps.onPointerUp),\n        };\n\n        if (render) return render(guarded, renderProps);\n\n        return isVirtualized ? (\n          <div {...(guarded as ComponentProps<\"div\">)} />\n        ) : (\n          <td {...guarded} />\n        );\n      }}\n    />\n  );\n}\n\nexport function TableSelectionCheckbox({\n  className,\n  ...props\n}: TableSelectionCheckboxProps): ReactElement {\n  // The box reads the Checkbox's size scale, from the table it sits in.\n  const size = useResolvedControlSize(useContext(TableSizeContext));\n  // React Aria counts every one of no rows as selected, so a table whose rows\n  // are gone while keys stay selected showed its select-all box checked. With\n  // no rows there is nothing to select: the box is unchecked, as TanStack\n  // Table's getIsAllRowsSelected() reports for an empty table, and React Aria\n  // keeps it disabled. Only the header box can render then.\n  const tableState = useContext(TableStateContext);\n  const noRows = tableState !== null && tableState.collection.size === 0;\n\n  return (\n    <AriaCheckbox\n      {...props}\n      {...(noRows ? { isIndeterminate: false, isSelected: false } : {})}\n      className={composeRenderProps(className, (value) =>\n        cn(\"sp-table-selection-checkbox\", value),\n      )}\n      data-size={size}\n      data-slot=\"table-selection-checkbox\"\n      slot=\"selection\"\n    >\n      {({ isIndeterminate, isSelected }) => (\n        // The indicator holds the state for the shared check motion in\n        // motion.css (rulebook M1): both glyphs stay mounted, and the check\n        // draws in and out as data-selected comes and goes, the dash as\n        // data-indeterminate does. An indeterminate box shows the dash only.\n        // The glyph takes no shared pop: that has no indeterminate state, so\n        // the dash would rest at the pop scale.\n        <span\n          aria-hidden=\"true\"\n          data-indeterminate={isIndeterminate || undefined}\n          data-selected={isSelected && !isIndeterminate ? true : undefined}\n          className=\"sp-table-selection-indicator\"\n          data-slot=\"table-selection-indicator\"\n        >\n          <CheckboxGlyph size={size} />\n        </span>\n      )}\n    </AriaCheckbox>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/table/table.tsx"},{"path":"packages/ui/src/components/table/table-shared.ts","content":"/**\n * Presentation shared by Table (React Aria) and NativeTable (native HTML).\n * Internal: it is not exported from the package index, so both families keep\n * one footer treatment and one cell alignment rule.\n */\n\nexport type TableAlign = \"center\" | \"end\" | \"start\";\n\n/** Summary rows: a top contour on the subtle surface at medium weight. */\nexport const tableFooterClassName =\n  \"border-t bg-surface-subtle font-medium [&>tr]:last:border-b-0\";\n\n/**\n * The size scale that table.css reads on the container: the heading row's\n * height and the cells' block padding, 32/4 px at sm, 36/6 at default, 40/8 at\n * lg and 48/12 at xl.\n */\nexport const tableContainerSizeClassName =\n  \"[--table-cell-padding-block:var(--sp-rw-space-1-5)] [--table-head-height:var(--sp-rw-space-9)] data-[size=sm]:[--table-cell-padding-block:var(--sp-rw-space-1)] data-[size=sm]:[--table-head-height:var(--sp-rw-space-8)] data-[size=lg]:[--table-cell-padding-block:var(--sp-rw-space-2)] data-[size=lg]:[--table-head-height:var(--sp-rw-space-10)] data-[size=xl]:[--table-cell-padding-block:var(--sp-rw-space-3)] data-[size=xl]:[--table-head-height:var(--sp-rw-space-12)]\";\n\n/**\n * Logical alignment follows the writing direction; end-aligned cells hold\n * numbers, so their digits are tabular.\n */\nexport function tableAlignmentClassName(align: TableAlign | undefined): string {\n  if (align === \"center\") return \"text-center\";\n  if (align === \"end\") return \"text-end tabular-nums\";\n  return \"text-start\";\n}\n","type":"registry:ui","target":"~/src/siteplane/components/table/table-shared.ts"},{"path":"packages/ui/src/components/table/table.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* The table fills its container; cells don't wrap, so columns that need more\n   * room scroll horizontally inside the container instead of the page. */\n  .sp-table {\n    background: var(--sp-rw-bg-secondary);\n  }\n\n  /* The heading row sticks to the top of the scrolling container (the\n   * container scrolls on both axes). It stays in view only when the container\n   * has a height limit, such as containerClassName=\"max-h-80\". */\n  .sp-table-header {\n    position: sticky;\n    z-index: 10;\n    top: 0;\n    background: var(--sp-rw-bg-secondary);\n    color: var(--sp-rw-fg-tertiary);\n    font-size: var(--sp-rw-font-14);\n    font-weight: 480;\n    line-height: var(--sp-rw-line-21);\n  }\n\n  /* The hover tint is a layer over the opaque fill, so a sticky heading row\n   * never turns see-through while it is hovered. */\n  :where(.sp-table[data-hoverable=\"true\"]) > .sp-table-header:hover {\n    background:\n      linear-gradient(var(--sp-rw-glow-hover), var(--sp-rw-glow-hover)),\n      var(--sp-rw-bg-secondary);\n  }\n\n  .sp-table-header > tr {\n    border-bottom: var(--sp-rw-border-default) solid\n      var(--sp-rw-border-translucent);\n  }\n\n  /* The heading row's height and the cells' block padding follow the size: the\n   * container sets them (tableContainerSizeClassName, table-shared.ts). */\n  .sp-table-head {\n    height: var(--table-head-height, var(--sp-rw-space-10));\n    color: var(--sp-rw-fg-tertiary);\n    font-weight: 480;\n    line-height: var(--sp-rw-line-21);\n    outline: none;\n  }\n\n  .sp-table-head {\n    padding: 0 var(--sp-rw-space-2);\n  }\n\n  /* lg and xl set a minimum row height on the container (--table-row-height);\n   * sm and the default size are as tall as their padding and line. */\n  .sp-table-cell {\n    height: var(--table-row-height, auto);\n    padding: var(--table-cell-padding-block, var(--sp-rw-space-2))\n      var(--sp-rw-space-2);\n  }\n\n  /* The kit focus token, as on the rows and the sortable headings. */\n  .sp-table-cell[data-focus-visible] {\n    box-shadow: inset 0 0 0 2px var(--sp-rw-border-focus);\n  }\n\n  .sp-table .sp-table-head,\n  .sp-table .sp-table-cell {\n    line-height: var(--sp-rw-line-21);\n  }\n\n  /* The outer columns take the inset of the surrounding surface, such as a\n   * CardFrameBleed, so a framed table lines up with the frame header and footer. */\n  .sp-table .sp-table-head:first-child,\n  .sp-table .sp-table-cell:first-child {\n    padding-inline-start: var(--table-edge-padding, var(--sp-rw-space-2));\n  }\n\n  .sp-table .sp-table-head:last-child,\n  .sp-table .sp-table-cell:last-child {\n    padding-inline-end: var(--table-edge-padding, var(--sp-rw-space-2));\n  }\n\n  .sp-table-row {\n    border-color: var(--sp-rw-border-translucent);\n  }\n\n  .sp-table-body .sp-table-row:not(:last-child) {\n    border-bottom-width: var(--sp-rw-border-default);\n  }\n\n  .sp-table-body .sp-table-row:last-child {\n    border-bottom-width: 0;\n  }\n\n  /* Rows take the rows ladder of the ghost controls (hover and selected\n   * steps of --sp-rw-interaction-*), in the control interaction colour. A\n   * selected row keeps its fill under the pointer and in focus: the stronger\n   * state wins over hover, as it does for pressed and open elsewhere. */\n  :where(.sp-table[data-hoverable=\"true\"])\n    > .sp-table-body\n    > .sp-table-row[data-hoverable=\"true\"]:not([data-selected]):hover {\n    background: color-mix(\n      in srgb,\n      var(--sp-rw-control-interaction)\n        calc(var(--sp-rw-interaction-hover) * 100%),\n      transparent\n    );\n  }\n\n  :where(.sp-table[data-hoverable=\"true\"])\n    > .sp-table-body\n    > .sp-table-row[data-interactive=\"true\"]:not([data-selected]):is(\n      :hover,\n      [data-hovered]\n    ),\n  .sp-table-body\n    .sp-table-row[data-interactive=\"true\"]:not([data-selected]):is(\n      :focus-within,\n      [data-focus-visible]\n    ) {\n    background: var(--sp-rw-bg-base);\n  }\n\n  .sp-table-row[data-selected] {\n    background: color-mix(\n      in srgb,\n      var(--sp-rw-control-interaction)\n        calc(var(--sp-rw-interaction-selected) * 100%),\n      transparent\n    );\n  }\n\n  .sp-table-row[data-focus-visible] {\n    box-shadow: inset 0 0 0 2px var(--sp-rw-border-focus);\n  }\n\n  .sp-table-row[data-disabled] {\n    cursor: not-allowed;\n    opacity: 64%;\n  }\n\n  .sp-table-row[data-interactive=\"true\"] {\n    cursor: pointer;\n  }\n\n  .sp-table-row[data-inactive] {\n    opacity: 72%;\n  }\n\n  .sp-table-head[data-allows-sorting] {\n    cursor: default;\n    pointer-events: none;\n  }\n\n  .sp-table-head-content {\n    gap: var(--sp-rw-space-1);\n  }\n\n  .sp-table-head[data-allows-sorting] .sp-table-head-content {\n    height: var(--sp-rw-space-8);\n    border-radius: var(--sp-rw-radius-control);\n    padding: var(--sp-rw-space-1-5) var(--sp-rw-space-2-5);\n    padding-inline-start: 0;\n    color: var(--sp-rw-fg-primary);\n    cursor: pointer;\n    pointer-events: auto;\n  }\n\n  .sp-table-head[data-focus-visible] .sp-table-head-content {\n    outline: 2px solid var(--sp-rw-border-focus);\n    outline-offset: 2px;\n  }\n\n  .sp-table-head[data-pressed] .sp-table-head-content {\n    opacity: 80%;\n  }\n\n  .sp-table-head-content svg {\n    width: var(--sp-rw-space-4);\n    height: var(--sp-rw-space-4);\n  }\n\n  /* The selection box is the Checkbox: its box, states, focus ring and size\n   * come from the Checkbox rules in design-rework.css. React Aria renders a\n   * label here, so the box only needs its layout. */\n  .sp-table-selection-checkbox {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    cursor: pointer;\n  }\n\n  .sp-table-selection-checkbox[data-disabled] {\n    cursor: not-allowed;\n  }\n\n  .sp-table-selection-indicator {\n    display: flex;\n    width: 100%;\n    height: 100%;\n    align-items: center;\n    justify-content: center;\n  }\n\n  html[data-motion-reduce=\"true\"] .sp-table-selection-checkbox,\n  [data-sp-motion=\"reduced\"] .sp-table-selection-checkbox {\n    transition-duration: 1ms;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/table/table.css"},{"path":"packages/ui/src/components/table/index.ts","content":"export * from \"./table\";\n","type":"registry:ui","target":"~/src/siteplane/components/table/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/table.md","categories":["Data display"],"meta":{"avoidWhen":["Avoid for a short list of labeled values or resources; use Settings List or Resource List."],"description":"React Aria table with composable rows and cells plus optional sorting, row or checkbox selection, captions and footers.","knownLimitations":["The table renders every row it is given and does not virtualize; page or virtualize large collections in the application."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["table","sorting","selection"],"useWhen":["Use when structured tabular data needs accessible navigation with optional sorting or selection."]}}