{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table",
  "title": "Table",
  "author": "mcpcn",
  "description": "Data table with optional single or multi-select modes for chat interfaces.",
  "dependencies": [
    "@base-ui/react",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "popover",
    "select",
    "input",
    "https://mcpcn.dev/r/apps-sdk-theme.json"
  ],
  "files": [
    {
      "path": "registry/list/table.tsx",
      "content": "\"use client\";\n\nimport {\n  Check,\n  ChevronDown,\n  ChevronLeft,\n  ChevronRight,\n  ChevronUp,\n  Copy,\n  Download,\n  RefreshCw,\n  Search,\n  Share2,\n} from \"lucide-react\";\nimport {\n  createContext,\n  createElement,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport type { ComponentProps, ImgHTMLAttributes, ReactNode } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\nconst BlockImage = (props: ImgHTMLAttributes<HTMLImageElement>) =>\n  createElement(\"img\", props);\n\ntype TableRow = Record<string, unknown>;\ntype SelectionMode = \"multi\" | \"none\" | \"single\";\n\nexport interface TableColumn<T = TableRow> {\n  accessor?: keyof T | string;\n  align?: \"center\" | \"left\" | \"right\";\n  header?: string;\n  render?: (value: unknown, row: T, index: number) => ReactNode;\n  sortable?: boolean;\n  width?: string;\n}\n\nexport interface TableProps<T = TableRow> {\n  actions?: {\n    onCopy?: (selectedRows: T[]) => void;\n    onDownload?: (selectedRows: T[]) => void;\n    onRefresh?: () => void;\n    onShare?: (selectedRows: T[]) => void;\n  };\n  appearance?: {\n    compact?: boolean;\n    displayMode?: \"fullscreen\" | \"inline\" | \"pip\";\n    emptyMessage?: string;\n    maxRows?: number;\n    selectable?: SelectionMode;\n    showActions?: boolean;\n    showFooter?: boolean;\n    showHeader?: boolean;\n    stickyHeader?: boolean;\n  };\n  children?: ReactNode;\n  className?: string;\n  control?: {\n    loading?: boolean;\n    selectedRows?: T[];\n  };\n  data?: {\n    columns?: TableColumn<T>[];\n    lastUpdated?: Date | string;\n    rows?: T[];\n    title?: string;\n    titleImage?: string;\n    totalRows?: number;\n  };\n}\n\ninterface TableContextValue {\n  columns: TableColumn<TableRow>[];\n  compact: boolean;\n  currentPage: number;\n  displayMode: \"fullscreen\" | \"inline\" | \"pip\";\n  emptyMessage: string;\n  lastUpdated?: Date | string;\n  loading: boolean;\n  maxRows: number;\n  onCopy?: (selectedRows: TableRow[]) => void;\n  onDownload?: (selectedRows: TableRow[]) => void;\n  onRefresh?: () => void;\n  onShare?: (selectedRows: TableRow[]) => void;\n  pageCount: number;\n  query: string;\n  rows: TableRow[];\n  selectable: SelectionMode;\n  selectedIndexes: Set<number>;\n  selectedRows: TableRow[];\n  setCurrentPage: (page: number) => void;\n  setQuery: (query: string) => void;\n  setSort: (accessor: string) => void;\n  showActions: boolean;\n  showFooter: boolean;\n  showHeader: boolean;\n  sort: { accessor: string; direction: \"asc\" | \"desc\" } | null;\n  stickyHeader: boolean;\n  title?: string;\n  titleImage?: string;\n  toggleAll: () => void;\n  toggleRow: (index: number) => void;\n  totalRows?: number;\n  visibleRows: TableRow[];\n}\n\nconst TableContext = createContext<TableContextValue | null>(null);\n\nexport const useTable = () => {\n  const context = useContext(TableContext);\n  if (!context) {\n    throw new Error(\"Table components must be used within Table\");\n  }\n  return context;\n};\n\nconst DEFAULT_COLUMNS: TableColumn<TableRow>[] = [\n  { accessor: \"name\", header: \"Name\", sortable: true },\n  { accessor: \"email\", header: \"Email\", sortable: true },\n  { accessor: \"status\", header: \"Status\", sortable: true },\n];\n\nconst DEFAULT_ROWS: TableRow[] = [\n  { email: \"john@example.com\", name: \"John Doe\", status: \"Active\" },\n  { email: \"jane@example.com\", name: \"Jane Smith\", status: \"Pending\" },\n  { email: \"bob@example.com\", name: \"Bob Johnson\", status: \"Active\" },\n];\n\nconst getValue = (row: TableRow, accessor: string) => {\n  const keys = accessor.split(\".\");\n  let value: unknown = row;\n  for (const key of keys) {\n    if (!(typeof value === \"object\" && value !== null)) {\n      return;\n    }\n    value = Object.hasOwn(value, key)\n      ? Object.getOwnPropertyDescriptor(value, key)?.value\n      : undefined;\n  }\n  return value;\n};\n\nconst formatValue = (value: unknown) => {\n  if (typeof value === \"number\") {\n    return new Intl.NumberFormat(\"en-US\").format(value);\n  }\n  return String(value ?? \"\");\n};\n\nconst formatTimestamp = (value: Date | string) => {\n  const date = value instanceof Date ? value : new Date(value);\n  return date.toLocaleString(\"en-US\", {\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"2-digit\",\n    month: \"short\",\n  });\n};\n\nconst TableSkeleton = ({ columns }: { columns: number }) => (\n  <>\n    {Array.from({ length: 3 }).map((_, row) => (\n      <tr className=\"border-b last:border-0\" key={row}>\n        {Array.from({ length: columns }).map((__, column) => (\n          <td className=\"px-3 py-3\" key={column}>\n            <div className=\"h-4 w-3/4 animate-pulse rounded bg-muted\" />\n          </td>\n        ))}\n      </tr>\n    ))}\n  </>\n);\n\nexport const TableActions = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<\"div\">) => {\n  const { onCopy, onDownload, onRefresh, onShare, selectedRows, showActions } =\n    useTable();\n  if (!(showActions || children)) {\n    return null;\n  }\n  return (\n    <div className={cn(\"flex items-center gap-1\", className)} {...props}>\n      {children ?? (\n        <>\n          <Button\n            aria-label=\"Copy\"\n            disabled={!onCopy}\n            onClick={() => onCopy?.(selectedRows)}\n            size=\"icon\"\n            variant=\"ghost\"\n          >\n            <Copy className=\"size-4\" />\n          </Button>\n          <Button\n            aria-label=\"Download\"\n            disabled={!onDownload}\n            onClick={() => onDownload?.(selectedRows)}\n            size=\"icon\"\n            variant=\"ghost\"\n          >\n            <Download className=\"size-4\" />\n          </Button>\n          <Button\n            aria-label=\"Share\"\n            disabled={!onShare}\n            onClick={() => onShare?.(selectedRows)}\n            size=\"icon\"\n            variant=\"ghost\"\n          >\n            <Share2 className=\"size-4\" />\n          </Button>\n          <Button\n            aria-label=\"Refresh\"\n            disabled={!onRefresh}\n            onClick={onRefresh}\n            size=\"icon\"\n            variant=\"ghost\"\n          >\n            <RefreshCw className=\"size-4\" />\n          </Button>\n        </>\n      )}\n    </div>\n  );\n};\n\nexport const TableHeader = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<\"header\">) => {\n  const { displayMode, query, setQuery, showHeader, title, titleImage } =\n    useTable();\n  if (!(showHeader || children)) {\n    return null;\n  }\n  return (\n    <header\n      className={cn(\n        \"flex flex-col gap-3 border-b p-3 sm:flex-row sm:items-center sm:justify-between\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <div className=\"flex min-w-0 items-center gap-2\">\n            {titleImage && (\n              <BlockImage\n                alt=\"\"\n                className=\"size-7 rounded object-cover\"\n                src={titleImage}\n              />\n            )}\n            {title && <h2 className=\"truncate font-semibold\">{title}</h2>}\n          </div>\n          <div className=\"flex items-center gap-2\">\n            {displayMode === \"fullscreen\" && (\n              <div className=\"relative min-w-52\">\n                <Search className=\"absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground\" />\n                <Input\n                  className=\"pl-8\"\n                  onChange={(event) => setQuery(event.target.value)}\n                  placeholder=\"Search rows...\"\n                  value={query}\n                />\n              </div>\n            )}\n            <TableActions />\n          </div>\n        </>\n      )}\n    </header>\n  );\n};\n\nconst SelectionCell = ({ index }: { index: number }) => {\n  const { selectedIndexes, toggleRow } = useTable();\n  const selected = selectedIndexes.has(index);\n  return (\n    <td className=\"px-3 py-3\">\n      <button\n        aria-label={selected ? \"Deselect row\" : \"Select row\"}\n        className={cn(\n          \"flex size-4 items-center justify-center rounded border\",\n          selected\n            ? \"border-foreground bg-foreground text-background\"\n            : \"border-border\"\n        )}\n        onClick={(event) => {\n          event.stopPropagation();\n          toggleRow(index);\n        }}\n        type=\"button\"\n      >\n        {selected && <Check className=\"size-3\" />}\n      </button>\n    </td>\n  );\n};\n\nconst SortIndicator = ({ accessor }: { accessor: string }) => {\n  const { sort } = useTable();\n  if (sort?.accessor !== accessor) {\n    return null;\n  }\n  return sort.direction === \"asc\" ? (\n    <ChevronUp className=\"size-3.5\" />\n  ) : (\n    <ChevronDown className=\"size-3.5\" />\n  );\n};\n\nexport const TableGrid = ({ className, ...props }: ComponentProps<\"div\">) => {\n  const {\n    columns,\n    compact,\n    emptyMessage,\n    loading,\n    selectable,\n    selectedIndexes,\n    setSort,\n    stickyHeader,\n    toggleAll,\n    toggleRow,\n    visibleRows,\n  } = useTable();\n  const columnCount = columns.length + (selectable === \"none\" ? 0 : 1);\n  return (\n    <div className={cn(\"overflow-x-auto\", className)} {...props}>\n      <table className=\"w-full text-sm\">\n        <thead\n          className={cn(\n            \"border-b bg-muted/40\",\n            stickyHeader && \"sticky top-0 z-10\"\n          )}\n        >\n          <tr>\n            {selectable !== \"none\" && (\n              <th className=\"w-10 px-3 py-2 text-left\">\n                {selectable === \"multi\" && (\n                  <button\n                    aria-label=\"Select all rows\"\n                    onClick={toggleAll}\n                    type=\"button\"\n                  >\n                    <span className=\"block size-4 rounded border border-border\" />\n                  </button>\n                )}\n              </th>\n            )}\n            {columns.map((column) => {\n              const accessor = String(column.accessor ?? \"\");\n              return (\n                <th\n                  className={cn(\n                    \"px-3 py-2 font-medium text-muted-foreground\",\n                    column.align === \"center\" && \"text-center\",\n                    column.align === \"right\" && \"text-right\",\n                    column.align !== \"center\" &&\n                      column.align !== \"right\" &&\n                      \"text-left\"\n                  )}\n                  key={accessor || column.header}\n                  style={{ width: column.width }}\n                >\n                  <button\n                    className={cn(\n                      \"inline-flex items-center gap-1\",\n                      column.sortable && \"cursor-pointer hover:text-foreground\"\n                    )}\n                    disabled={!column.sortable}\n                    onClick={() => setSort(accessor)}\n                    type=\"button\"\n                  >\n                    {column.header}\n                    <SortIndicator accessor={accessor} />\n                  </button>\n                </th>\n              );\n            })}\n          </tr>\n        </thead>\n        <tbody>\n          {loading && <TableSkeleton columns={columnCount} />}\n          {!loading && visibleRows.length === 0 && (\n            <tr>\n              <td\n                className=\"px-3 py-8 text-center text-muted-foreground\"\n                colSpan={columnCount}\n              >\n                {emptyMessage}\n              </td>\n            </tr>\n          )}\n          {!loading &&\n            visibleRows.map((row, index) => (\n              <tr\n                aria-selected={selectedIndexes.has(index)}\n                className={cn(\n                  \"border-b transition-colors last:border-0\",\n                  selectable !== \"none\" && \"cursor-pointer hover:bg-muted/30\",\n                  selectedIndexes.has(index) && \"bg-muted/30\"\n                )}\n                key={index}\n                onClick={() => toggleRow(index)}\n              >\n                {selectable !== \"none\" && <SelectionCell index={index} />}\n                {columns.map((column, columnIndex) => {\n                  const value = getValue(row, String(column.accessor ?? \"\"));\n                  return (\n                    <td\n                      className={cn(\n                        \"px-3\",\n                        compact ? \"py-2\" : \"py-3\",\n                        column.align === \"center\" && \"text-center\",\n                        column.align === \"right\" && \"text-right\",\n                        columnIndex === 0 && \"font-medium\"\n                      )}\n                      key={String(column.accessor ?? columnIndex)}\n                    >\n                      {column.render\n                        ? column.render(value, row, index)\n                        : formatValue(value)}\n                    </td>\n                  );\n                })}\n              </tr>\n            ))}\n        </tbody>\n      </table>\n    </div>\n  );\n};\n\nexport const TableFooter = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<\"footer\">) => {\n  const {\n    currentPage,\n    displayMode,\n    lastUpdated,\n    maxRows,\n    pageCount,\n    rows,\n    setCurrentPage,\n    showFooter,\n    totalRows,\n  } = useTable();\n  if (!(showFooter || children)) {\n    return null;\n  }\n  const more = Math.max(0, (totalRows ?? rows.length) - maxRows);\n  return (\n    <footer\n      className={cn(\n        \"flex items-center justify-between border-t px-3 py-2 text-muted-foreground text-xs\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <span>\n            {lastUpdated ? `Updated ${formatTimestamp(lastUpdated)}` : null}\n            {displayMode !== \"fullscreen\" && more > 0\n              ? ` · +${more} more`\n              : null}\n          </span>\n          {displayMode === \"fullscreen\" && pageCount > 1 && (\n            <div className=\"flex items-center gap-2\">\n              <Button\n                aria-label=\"Previous page\"\n                disabled={currentPage === 1}\n                onClick={() => setCurrentPage(currentPage - 1)}\n                size=\"icon\"\n                variant=\"outline\"\n              >\n                <ChevronLeft className=\"size-4\" />\n              </Button>\n              <span>\n                {currentPage} / {pageCount}\n              </span>\n              <Button\n                aria-label=\"Next page\"\n                disabled={currentPage === pageCount}\n                onClick={() => setCurrentPage(currentPage + 1)}\n                size=\"icon\"\n                variant=\"outline\"\n              >\n                <ChevronRight className=\"size-4\" />\n              </Button>\n            </div>\n          )}\n        </>\n      )}\n    </footer>\n  );\n};\n\nexport const TableContent = ({\n  children,\n  className,\n  ...props\n}: ComponentProps<\"div\"> & { children: React.ReactNode }) => (\n  <div className={className} {...props}>\n    {children}\n  </div>\n);\n\nconst useProcessedRows = (\n  columns: TableColumn<TableRow>[],\n  rows: TableRow[],\n  query: string,\n  sort: TableContextValue[\"sort\"]\n) =>\n  useMemo(() => {\n    const normalizedQuery = query.trim().toLowerCase();\n    const filtered = normalizedQuery\n      ? rows.filter((row) =>\n          columns.some((column) =>\n            formatValue(getValue(row, String(column.accessor ?? \"\")))\n              .toLowerCase()\n              .includes(normalizedQuery)\n          )\n        )\n      : rows;\n    if (!sort) {\n      return filtered;\n    }\n    return [...filtered].toSorted((left, right) => {\n      const leftValue = getValue(left, sort.accessor);\n      const rightValue = getValue(right, sort.accessor);\n      const comparison = String(leftValue ?? \"\").localeCompare(\n        String(rightValue ?? \"\"),\n        undefined,\n        { numeric: true }\n      );\n      return sort.direction === \"asc\" ? comparison : -comparison;\n    });\n  }, [columns, query, rows, sort]);\n\nconst useTableSelection = (\n  rows: TableRow[],\n  visibleRows: TableRow[],\n  selectable: SelectionMode\n) => {\n  const [selectedIndexes, setSelectedIndexes] = useState<Set<number>>(\n    new Set()\n  );\n  const selectedRows = rows.filter((_, index) => selectedIndexes.has(index));\n  const toggleRow = (index: number) => {\n    if (selectable === \"none\") {\n      return;\n    }\n    setSelectedIndexes((current) => {\n      const next = new Set(selectable === \"single\" ? [] : current);\n      if (next.has(index)) {\n        next.delete(index);\n      } else {\n        next.add(index);\n      }\n      return next;\n    });\n  };\n  const toggleAll = () => {\n    setSelectedIndexes((current) =>\n      current.size === visibleRows.length\n        ? new Set()\n        : new Set(visibleRows.map((_, index) => index))\n    );\n  };\n  return { selectedIndexes, selectedRows, toggleAll, toggleRow };\n};\n\nconst resolveTableAppearance = (\n  appearance: TableProps<TableRow>[\"appearance\"]\n) => ({\n  compact: appearance?.compact ?? false,\n  displayMode: appearance?.displayMode ?? \"inline\",\n  emptyMessage: appearance?.emptyMessage ?? \"No data available\",\n  maxRows: appearance?.maxRows ?? 5,\n  selectable: appearance?.selectable ?? \"none\",\n  showActions: appearance?.showActions ?? true,\n  showFooter: appearance?.showFooter ?? true,\n  showHeader: appearance?.showHeader ?? true,\n  stickyHeader: appearance?.stickyHeader ?? false,\n});\n\nconst resolveTableData = (data: TableProps<TableRow>[\"data\"]) => ({\n  columns: data?.columns ?? DEFAULT_COLUMNS,\n  lastUpdated: data?.lastUpdated,\n  rows: data?.rows ?? DEFAULT_ROWS,\n  title: data?.title,\n  titleImage: data?.titleImage,\n  totalRows: data?.totalRows,\n});\n\nconst TableRoot = ({\n  actions,\n  appearance,\n  children,\n  className,\n  control,\n  data,\n}: TableProps<TableRow> & { children: React.ReactNode }) => {\n  const {\n    compact,\n    displayMode,\n    emptyMessage,\n    maxRows,\n    selectable,\n    showActions,\n    showFooter,\n    showHeader,\n    stickyHeader,\n  } = resolveTableAppearance(appearance);\n  const { columns, lastUpdated, rows, title, titleImage, totalRows } =\n    resolveTableData(data);\n  const rowsPerPage = 15;\n  const [query, setQuery] = useState(\"\");\n  const [sort, setSortState] = useState<TableContextValue[\"sort\"]>(null);\n  const [currentPage, setCurrentPage] = useState(1);\n  const processedRows = useProcessedRows(columns, rows, query, sort);\n\n  const pageCount = Math.max(1, Math.ceil(processedRows.length / rowsPerPage));\n  const visibleRows =\n    displayMode === \"fullscreen\"\n      ? processedRows.slice(\n          (currentPage - 1) * rowsPerPage,\n          currentPage * rowsPerPage\n        )\n      : processedRows.slice(0, maxRows);\n  const { selectedIndexes, selectedRows, toggleAll, toggleRow } =\n    useTableSelection(rows, visibleRows, selectable);\n\n  const context: TableContextValue = {\n    columns,\n    compact,\n    currentPage,\n    displayMode,\n    emptyMessage,\n    lastUpdated,\n    loading: control?.loading ?? false,\n    maxRows,\n    onCopy: actions?.onCopy,\n    onDownload: actions?.onDownload,\n    onRefresh: actions?.onRefresh,\n    onShare: actions?.onShare,\n    pageCount,\n    query,\n    rows,\n    selectable,\n    selectedIndexes,\n    selectedRows,\n    setCurrentPage,\n    setQuery,\n    setSort: (accessor) =>\n      setSortState((current) => ({\n        accessor,\n        direction:\n          current?.accessor === accessor && current.direction === \"asc\"\n            ? \"desc\"\n            : \"asc\",\n      })),\n    showActions,\n    showFooter,\n    showHeader,\n    sort,\n    stickyHeader,\n    title,\n    titleImage,\n    toggleAll,\n    toggleRow,\n    totalRows,\n    visibleRows,\n  };\n\n  return (\n    <TableContext.Provider value={context}>\n      <div\n        className={cn(\"overflow-hidden rounded-lg border bg-card\", className)}\n      >\n        {children}\n      </div>\n    </TableContext.Provider>\n  );\n};\n\nexport const Table = TableRoot;\n",
      "type": "registry:block",
      "target": "components/ui/table.tsx"
    }
  ],
  "categories": [
    "list"
  ],
  "type": "registry:block"
}