{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-selection",
  "type": "registry:component",
  "title": "Table selection",
  "description": "A checkbox column for a data table",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/data/src/internal/size.ts",
      "target": "@ui/gpuixui/data/internal/size.ts",
      "type": "registry:lib",
      "content": "import * as Option from \"effect/Option\";\n\nexport const paintableSize = (\n  size: number | undefined,\n): Option.Option<number> =>\n  Option.filter(\n    Option.fromNullishOr(size),\n    (value) => Number.isFinite(value) && value >= 0,\n  );\n"
    },
    {
      "path": "packages/data/src/recipe.ts",
      "target": "@ui/gpuixui/data/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(256);\n"
    },
    {
      "path": "packages/data/src/table/frame.tsx",
      "target": "@ui/gpuixui/data/table/frame.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport type { EventPayload } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\n\nexport const DEFAULT_HEADER_HEIGHT = 44;\n\nexport interface TableFrame {\n  readonly viewportHeight: Option.Option<number>;\n\n  readonly headerHeight: Option.Option<number>;\n  readonly reportViewport: (height: number) => void;\n  readonly reportHeader: (height: number) => void;\n}\n\nconst TableFrameContext = createContext<Option.Option<TableFrame>>(\n  Option.none(),\n);\n\nexport const useTableFrame = (): Option.Option<TableFrame> =>\n  useContext(TableFrameContext);\n\nexport const useMeasuredBodyHeight = (): Option.Option<number> => {\n  const frame = useTableFrame();\n  return Option.flatMap(frame, (value) =>\n    Option.map(value.viewportHeight, (viewport) =>\n      Math.max(0, viewport - Option.getOrElse(value.headerHeight, () => 0)),\n    ),\n  );\n};\n\nconst useHeightReport = (\n  initial?: number,\n): [Option.Option<number>, (height: number) => void] => {\n  const [height, setHeight] = useState<Option.Option<number>>(\n    Option.fromNullishOr(initial),\n  );\n  const report = useCallback((next: number) => {\n    if (!Number.isFinite(next) || next <= 0) return;\n    setHeight((current) =>\n      Option.contains(current, next) ? current : Option.some(next),\n    );\n  }, []);\n  return [height, report];\n};\n\nexport const TableFrameProvider = ({\n  children,\n  initialHeight,\n}: {\n  readonly children: ReactNode;\n\n  readonly initialHeight?: number;\n}) => {\n  const [headerHeight, reportHeader] = useHeightReport(DEFAULT_HEADER_HEIGHT);\n  const [viewportHeight, reportViewport] = useHeightReport(initialHeight);\n  const frame = useMemo<TableFrame>(\n    () => ({\n      headerHeight,\n      viewportHeight,\n      reportHeader,\n      reportViewport,\n    }),\n    [headerHeight, viewportHeight, reportHeader, reportViewport],\n  );\n  return (\n    <TableFrameContext.Provider value={Option.some(frame)}>\n      {children}\n    </TableFrameContext.Provider>\n  );\n};\n\nexport const reportBoth = (\n  event: EventPayload,\n  caller: ((event: EventPayload) => void) | undefined,\n  report: (height: number) => void,\n): void => {\n  caller?.(event);\n  report(event.height ?? 0);\n};\n"
    },
    {
      "path": "packages/data/src/table/recipe.ts",
      "target": "@ui/gpuixui/data/table/recipe.ts",
      "type": "registry:lib",
      "content": "export const TABLE_ROOT =\n  \"flex flex-col overflow-hidden rounded-lg border border-input bg-card text-foreground\";\n\nexport const TABLE_VIEWPORT = \"flex flex-col flex-1 min-h-0\";\n\nexport const TABLE_HEADER = \"flex flex-col shrink-0\";\n\nexport const TABLE_BODY = \"flex flex-col flex-1 min-h-0\";\n\nexport const TABLE_ROW = \"flex flex-row items-center shrink-0\";\n\nexport const TABLE_ROW_SELECTED =\n  \"flex flex-row items-center shrink-0 bg-selected text-selected-foreground\";\n\nexport const TABLE_ROW_INTERACTIVE =\n  \"flex flex-row items-center shrink-0 hover:bg-hover cursor-pointer\";\n\nexport const TABLE_HEADER_CELL =\n  \"relative flex flex-row items-center px-3 py-2 text-sm font-medium text-muted-foreground\";\n\nexport const TABLE_COLUMN_DIVIDER =\n  \"absolute inset-y-0 right-0 w-1.5 bg-border hover:bg-primary\";\n\nexport const TABLE_CELL = \"px-3 py-2 text-sm\";\n\nexport const TABLE_DETAIL_ROW =\n  \"flex flex-row items-stretch shrink-0 bg-muted text-muted-foreground\";\n\nexport const TABLE_DETAIL_CELL = \"px-3 py-3 text-sm\";\n\nexport const TABLE_CAPTION = \"px-3 py-2 text-xs text-muted-foreground\";\n\nexport const TABLE_EMPTY =\n  \"flex flex-col items-center justify-center gap-1 px-3 py-6\";\n\nexport const TABLE_SKELETON_BAR = \"h-2 rounded-sm bg-skeleton\";\n"
    },
    {
      "path": "packages/data/src/table/table.tsx",
      "target": "@ui/gpuixui/data/table/table.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { Props, PublicInstance, StyleDesc } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport {\n  borderWidthOf,\n  spaceValue,\n  spokenText,\n} from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport type { ReactNode, Ref } from \"react\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { recipeStyle } from \"../recipe.js\";\nimport {\n  dividerOn,\n  pointerWash,\n  textStyle,\n  themeColors,\n  useTheme,\n} from \"../theme.js\";\nimport {\n  reportBoth,\n  TableFrameProvider,\n  useMeasuredBodyHeight,\n  useTableFrame,\n} from \"./frame.js\";\nimport {\n  TABLE_BODY,\n  TABLE_CAPTION,\n  TABLE_CELL,\n  TABLE_EMPTY,\n  TABLE_HEADER,\n  TABLE_HEADER_CELL,\n  TABLE_ROOT,\n  TABLE_ROW,\n  TABLE_ROW_INTERACTIVE,\n  TABLE_ROW_SELECTED,\n  TABLE_SKELETON_BAR,\n  TABLE_VIEWPORT,\n} from \"./recipe.js\";\n\nexport type CellAlign = \"left\" | \"center\" | \"right\";\n\nconst JUSTIFY: Readonly<Record<CellAlign, string>> = {\n  left: \"flex-start\",\n  center: \"center\",\n  right: \"flex-end\",\n};\n\nexport interface CellLayoutProps {\n  readonly width?: number;\n\n  readonly align?: CellAlign;\n\n  readonly className?: string;\n}\n\nconst cellLayout = ({ width, align = \"left\" }: CellLayoutProps): StyleDesc => {\n  const track = Option.getOrUndefined(paintableSize(width));\n  return {\n    ...(track === undefined\n      ? { flexGrow: 1, flexShrink: 1, flexBasis: 0, minWidth: 0 }\n      : { width: track, flexGrow: 0, flexShrink: 0, flexBasis: track }),\n    justifyContent: JUSTIFY[align],\n  };\n};\n\nexport interface TableProps extends Omit<Props, \"children\"> {\n  readonly height?: number;\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const Table = ({\n  height,\n  className,\n  children,\n  style,\n  ...rest\n}: TableProps) => {\n  const theme = useTheme();\n  const bound = Option.getOrUndefined(paintableSize(height));\n  return (\n    <TableFrameProvider initialHeight={bound}>\n      <div\n        {...rest}\n        role=\"table\"\n        style={mergeStyles(\n          mergeStyles(\n            recipeStyle(cn(TABLE_ROOT, className), theme),\n            bound === undefined ? undefined : { height: bound },\n          ),\n          style,\n        )}\n      >\n        {children}\n      </div>\n    </TableFrameProvider>\n  );\n};\n\nexport interface TableViewportProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableViewport = ({\n  className,\n  children,\n  style,\n  onLayout,\n  ...rest\n}: TableViewportProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const frame = useTableFrame();\n  return (\n    <div\n      {...rest}\n      onLayout={(event) =>\n        reportBoth(\n          event,\n          onLayout,\n          Option.match(frame, {\n            onNone: () => () => {},\n            onSome: (measured) => measured.reportViewport,\n          }),\n        )\n      }\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(TABLE_VIEWPORT, className), theme), {\n          overflowX: \"scroll\",\n          scrollbarColor: colors.scrollbar,\n        }),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableHeaderProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableHeader = ({\n  className,\n  children,\n  style,\n  onLayout,\n  ...rest\n}: TableHeaderProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const frame = useTableFrame();\n  return (\n    <div\n      {...rest}\n      onLayout={(event) =>\n        reportBoth(\n          event,\n          onLayout,\n          Option.match(frame, {\n            onNone: () => () => {},\n            onSome: (measured) => measured.reportHeader,\n          }),\n        )\n      }\n      role=\"rowgroup\"\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(TABLE_HEADER, className), theme), {\n          borderBottomWidth: borderWidthOf(theme, \"thin\"),\n\n          borderColor: dividerOn(theme, colors.card) ?? colors.border,\n        }),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableBodyProps extends Omit<Props, \"children\"> {\n  readonly rows?: number;\n\n  readonly height?: number;\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableBody = ({\n  rows = 3,\n  height,\n  className,\n  children,\n  style,\n  ...rest\n}: TableBodyProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const empty = children === undefined || children === null;\n  const measured = useMeasuredBodyHeight();\n  const definite = Option.orElse(paintableSize(height), () => measured);\n  return (\n    <div\n      {...rest}\n      role=\"rowgroup\"\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(TABLE_BODY, className), theme), {\n          overflowY: \"scroll\",\n          scrollbarColor: colors.scrollbar,\n          ...(Option.isNone(definite)\n            ? {}\n            : {\n                height: definite.value,\n                flexGrow: 0,\n                flexShrink: 0,\n                flexBasis: definite.value,\n              }),\n        }),\n        style,\n      )}\n    >\n      {empty ? <TableSkeleton rows={rows} /> : children}\n    </div>\n  );\n};\n\nexport interface TableRowProps extends Omit<Props, \"children\"> {\n  readonly selected?: boolean;\n\n  readonly disabled?: boolean;\n\n  readonly instanceRef?: Ref<PublicInstance>;\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableRow = ({\n  selected,\n  disabled,\n  instanceRef,\n  className,\n  children,\n  style,\n  onClick,\n  ...rest\n}: TableRowProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const interactive = onClick !== undefined && disabled !== true;\n  const recipe =\n    selected === true\n      ? TABLE_ROW_SELECTED\n      : interactive\n        ? TABLE_ROW_INTERACTIVE\n        : TABLE_ROW;\n\n  const rule = (surface: string | undefined): string | undefined =>\n    dividerOn(theme, surface) ?? colors.border;\n  const hoverFill = selected === true ? colors.accent : colors.hover;\n  const pressFill =\n    (selected === true\n      ? pointerWash(theme, \"active\", \"accent\")\n      : pointerWash(theme, \"active\", \"hover\")) ?? colors.active;\n  return (\n    <div\n      {...rest}\n      ref={instanceRef}\n      role=\"row\"\n      aria-selected={selected}\n      onClick={disabled === true ? undefined : onClick}\n      style={mergeStyles(\n        mergeStyles(recipeStyle(cn(recipe, className), theme), {\n          borderBottomWidth: borderWidthOf(theme, \"thin\"),\n          borderColor: rule(selected === true ? colors.selected : colors.card),\n\n          ...(interactive\n            ? {\n                cursor: \"pointer\",\n\n                hover:\n                  theme.interaction === \"appkit\"\n                    ? {}\n                    : {\n                        backgroundColor: hoverFill,\n                        borderColor: rule(hoverFill),\n                      },\n                active: {\n                  backgroundColor: pressFill,\n                  borderColor: rule(pressFill),\n                },\n              }\n            : {}),\n        }),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableHeaderCellProps\n  extends Omit<Props, \"children\">, CellLayoutProps {\n  readonly children?: ReactNode;\n}\n\nexport const TableHeaderCell = ({\n  width,\n  align = \"left\",\n  className,\n  children,\n  style,\n  ...rest\n}: TableHeaderCellProps) => {\n  const theme = useTheme();\n  return (\n    <div\n      {...rest}\n      role=\"columnheader\"\n      style={mergeStyles(\n        mergeStyles(\n          recipeStyle(cn(TABLE_HEADER_CELL, className), theme),\n          cellLayout({ width, align }),\n        ),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableCellProps\n  extends Omit<Props, \"children\">, CellLayoutProps {\n  readonly children?: ReactNode;\n}\n\nexport const TableCell = ({\n  width,\n  align = \"left\",\n  className,\n  children,\n  style,\n  ...rest\n}: TableCellProps) => {\n  const theme = useTheme();\n  return (\n    <div\n      {...rest}\n      role=\"cell\"\n      style={mergeStyles(\n        mergeStyles(\n          recipeStyle(cn(TABLE_CELL, className), theme),\n          cellLayout({ width, align }),\n        ),\n        style,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport interface TableCaptionProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const TableCaption = ({\n  className,\n  children,\n  style,\n  ...rest\n}: TableCaptionProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  return (\n    <div\n      {...rest}\n      style={mergeStyles(\n        recipeStyle(cn(TABLE_CAPTION, className), theme),\n        style,\n      )}\n    >\n      {typeof children === \"string\" || typeof children === \"number\" ? (\n        <text\n          style={{\n            ...textStyle(theme, \"caption\"),\n            color: colors.mutedForeground,\n          }}\n        >\n          {children}\n        </text>\n      ) : (\n        children\n      )}\n    </div>\n  );\n};\n\nexport interface TableEmptyProps extends Omit<Props, \"children\"> {\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport const DEFAULT_EMPTY_MESSAGE = \"No rows\";\n\nexport const emptyMessage = (empty: ReactNode): ReactNode =>\n  P.isString(empty)\n    ? spokenText(Option.fromNullishOr(empty), DEFAULT_EMPTY_MESSAGE)\n    : (empty ?? DEFAULT_EMPTY_MESSAGE);\n\nexport const TableEmpty = ({\n  className,\n  children,\n  style,\n  ...rest\n}: TableEmptyProps) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  return (\n    <div\n      {...rest}\n      role=\"row\"\n      style={mergeStyles(recipeStyle(cn(TABLE_EMPTY, className), theme), style)}\n    >\n      <div\n        role=\"cell\"\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          gap: spaceValue(theme, \"xs\"),\n        }}\n      >\n        {typeof children === \"string\" || typeof children === \"number\" ? (\n          <text\n            style={{\n              ...textStyle(theme, \"body\"),\n              color: colors.mutedForeground,\n            }}\n          >\n            {children}\n          </text>\n        ) : (\n          children\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport interface TableSkeletonProps {\n  readonly rows?: number;\n\n  readonly columns?: number;\n}\n\nexport const SKELETON_TRACK_LIMIT = 20;\n\nconst trackCount = (count: number, fallback: number): number =>\n  Number.isFinite(count)\n    ? Math.min(SKELETON_TRACK_LIMIT, Math.max(1, Math.floor(count)))\n    : fallback;\n\nexport const TableSkeleton = ({\n  rows = 3,\n  columns = 3,\n}: TableSkeletonProps) => {\n  const rowCount = trackCount(rows, 3);\n  const columnCount = trackCount(columns, 3);\n  return (\n    <>\n      {Array.from({ length: rowCount }, (_, row) => (\n        <TableRow key={`skeleton-${row}`}>\n          {Array.from({ length: columnCount }, (_, column) => (\n            <TableCell key={`skeleton-${row}-${column}`}>\n              <div style={{ display: \"flex\", flexDirection: \"column\" }}>\n                <SkeletonBar />\n              </div>\n            </TableCell>\n          ))}\n        </TableRow>\n      ))}\n    </>\n  );\n};\n\nconst SkeletonBar = () => {\n  const theme = useTheme();\n  return (\n    <div\n      style={mergeStyles(recipeStyle(TABLE_SKELETON_BAR, theme), {\n        width: \"60%\",\n      })}\n    />\n  );\n};\n"
    },
    {
      "path": "packages/data/src/theme.ts",
      "target": "@ui/gpuixui/data/theme.ts",
      "type": "registry:lib",
      "content": "import type { Theme } from \"../tokens/index.js\";\nimport { useTheme as useTokenTheme } from \"../tokens/react.js\";\n\nexport {\n  controlFocusRing,\n  dividerOn,\n  pointerWash,\n  textStyle,\n  themeColors,\n} from \"../tokens/index.js\";\nexport type { Theme };\n\nexport const useTheme = (): Theme => useTokenTheme();\n"
    }
  ],
  "docs": "[Table selection](https://gpuixui.sudokw.com/data-display/table-selection) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/data-display/table-selection",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}