{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table",
  "title": "Table",
  "description": "Real table markup with sortable headers, a sticky head, selection state, an empty row and right-aligned figures, in plain, striped, card or compact, scrolling inside itself when narrow.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/table.tsx",
      "content": "import type * as React from \"react\"\r\nimport { useContext, useEffect, useMemo, useRef, useState } from \"react\"\r\nimport { TableContext, type TableVariant } from \"@/registry/miseui/ui/table-context\"\r\nimport { TableHead, TableSortHead, type TableHeadProps, type TableSortHeadProps } from \"@/registry/miseui/ui/table-head\"\r\nimport { RowHighlight, useRowHighlight } from \"@/registry/miseui/ui/table-highlight\"\r\nimport {\r\n  nextSort,\r\n  sortRows,\r\n  useTableSelection,\r\n  useTableSort,\r\n  type SortAccessor,\r\n  type SortDirection,\r\n  type TableSort,\r\n} from \"@/registry/miseui/ui/table-state\"\r\nimport { cn } from \"@/registry/miseui/lib/utils\"\r\n\r\ntype TableEffect = \"spring\"\r\n\r\nconst SCROLLER_CLASS: Record<TableVariant, string> = {\r\n  plain: \"\",\r\n  striped: \"\",\r\n  card: \"rounded-lg border bg-card\",\r\n  compact: \"\",\r\n}\r\n\r\nconst CELL_CLASS: Record<TableVariant, string> = {\r\n  plain: \"h-12 px-3\",\r\n  striped: \"h-12 px-3 first:rounded-l-md last:rounded-r-md\",\r\n  card: \"h-14 px-4\",\r\n  compact: \"h-8 px-2 pointer-coarse:h-10\",\r\n}\r\n\r\n/** Row lines sit on the cells: `border-separate` is what lets a sticky head keep its own line while scrolling. */\r\nconst ROW_LINE_CLASS = \"[tbody_&:not(:last-child)>td]:border-b\"\r\nconst ROW_CLASS: Record<TableVariant, string> = {\r\n  plain: ROW_LINE_CLASS,\r\n  striped: \"[tbody_&]:odd:bg-muted\",\r\n  card: ROW_LINE_CLASS,\r\n  compact: \"\",\r\n}\r\n\r\nconst ROW_HOVER_CLASS = \"transition-colors duration-(--duration-fast) ease-(--ease-mise) [tbody_&]:hover:bg-accent\"\r\n\r\n/** A scroller with nothing focusable inside cannot be reached by keyboard, so it becomes a tab stop once it overflows. */\r\nfunction useOverflowing(ref: React.RefObject<HTMLDivElement | null>) {\r\n  const [overflowing, setOverflowing] = useState(false)\r\n  useEffect(() => {\r\n    const element = ref.current\r\n    // A test environment without it should get a plain table, not a crash.\r\n    if (!element || typeof ResizeObserver === \"undefined\") return\r\n    const check = () => {\r\n      setOverflowing(element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight)\r\n    }\r\n    const observer = new ResizeObserver(check)\r\n    observer.observe(element)\r\n    // The scroller keeps its size when rows arrive, so the table has to be watched too.\r\n    const table = element.querySelector(\"table\")\r\n    if (table) observer.observe(table)\r\n    return () => observer.disconnect()\r\n  }, [ref])\r\n  return overflowing\r\n}\r\n\r\ntype TableProps = React.ComponentProps<\"div\"> & {\r\n  /** `plain` hairlines, `striped` alternate rows, `card` a framed surface with a tinted head, `compact` dense rows. */\r\n  variant?: TableVariant\r\n  /** `spring` slides one highlight between rows as the pointer or focus moves. */\r\n  effect?: TableEffect\r\n  /** Keeps the head in view. Give the table a height too, such as `className=\"max-h-80\"`. */\r\n  stickyHeader?: boolean\r\n  /** Goes on the `<table>` itself. `className` goes on the scrolling root. */\r\n  tableClassName?: string\r\n  /** Names the scroll area for assistive tech when it overflows. */\r\n  scrollLabel?: string\r\n  /** Names the `<table>`. A TableCaption does the same with visible text. */\r\n  label?: string\r\n}\r\n\r\nfunction Table({ variant = \"plain\", effect, stickyHeader = false, tableClassName, scrollLabel = \"Table\", label, className, children, ...props }: TableProps) {\r\n  const scroller = useRef<HTMLDivElement>(null)\r\n  const overflowing = useOverflowing(scroller)\r\n  const spring = effect === \"spring\"\r\n  const highlight = useRowHighlight(spring)\r\n  const context = useMemo(() => ({ variant, sticky: stickyHeader, spring }), [variant, stickyHeader, spring])\r\n  return (\r\n    <TableContext.Provider value={context}>\r\n      <div\r\n        ref={scroller}\r\n        data-slot=\"table\"\r\n        data-variant={variant}\r\n        data-effect={effect}\r\n        role={overflowing ? \"region\" : undefined}\r\n        aria-label={overflowing ? scrollLabel : undefined}\r\n        tabIndex={overflowing ? 0 : undefined}\r\n        className={cn(\"relative w-full overflow-auto outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\", SCROLLER_CLASS[variant], className)}\r\n        {...props}\r\n        {...highlight.handlers}\r\n      >\r\n        {spring ? <RowHighlight state={highlight.state} /> : null}\r\n        <table\r\n          aria-label={label}\r\n          data-slot=\"table-element\"\r\n          className={cn(\"relative z-1 w-full border-separate border-spacing-0 text-left\", variant === \"compact\" ? \"text-[13px]\" : \"text-sm\", tableClassName)}\r\n        >\r\n          {children}\r\n        </table>\r\n      </div>\r\n    </TableContext.Provider>\r\n  )\r\n}\r\n\r\nfunction TableHeader(props: React.ComponentProps<\"thead\">) {\r\n  return <thead data-slot=\"table-header\" {...props} />\r\n}\r\n\r\nfunction TableBody(props: React.ComponentProps<\"tbody\">) {\r\n  return <tbody data-slot=\"table-body\" {...props} />\r\n}\r\n\r\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\r\n  return <tfoot data-slot=\"table-footer\" className={cn(\"font-medium [&_td]:border-t\", className)} {...props} />\r\n}\r\n\r\nfunction TableCaption({ className, ...props }: React.ComponentProps<\"caption\">) {\r\n  return <caption data-slot=\"table-caption\" className={cn(\"mt-3 caption-bottom text-[13px] text-muted-foreground\", className)} {...props} />\r\n}\r\n\r\ntype TableRowProps = React.ComponentProps<\"tr\"> & {\r\n  /** Tints the row. The Checkbox in the row is what tells assistive tech. */\r\n  selected?: boolean\r\n}\r\n\r\nfunction TableRow({ selected = false, className, ...props }: TableRowProps) {\r\n  const { variant, spring } = useContext(TableContext)\r\n  return (\r\n    <tr\r\n      data-slot=\"table-row\"\r\n      data-selected={selected ? \"true\" : undefined}\r\n      // With the spring on, the sliding band already says which row is under the pointer.\r\n      className={cn(!spring && ROW_HOVER_CLASS, ROW_CLASS[variant], \"data-[selected=true]:bg-mise-soft!\", className)}\r\n      {...props}\r\n    />\r\n  )\r\n}\r\n\r\ntype TableCellProps = React.ComponentProps<\"td\"> & {\r\n  /** Right-aligned tabular figures, so digits of equal place line up down the column. */\r\n  numeric?: boolean\r\n}\r\n\r\nfunction TableCell({ numeric = false, className, ...props }: TableCellProps) {\r\n  const { variant } = useContext(TableContext)\r\n  return <td data-slot=\"table-cell\" className={cn(CELL_CLASS[variant], numeric && \"text-right tabular-nums\", className)} {...props} />\r\n}\r\n\r\ntype TableEmptyProps = Omit<React.ComponentProps<\"td\">, \"colSpan\"> & {\r\n  /** How many columns the message spans. */\r\n  colSpan: number\r\n}\r\n\r\nfunction TableEmpty({ colSpan, className, ...props }: TableEmptyProps) {\r\n  return (\r\n    <tr data-slot=\"table-empty\">\r\n      <td colSpan={colSpan} className={cn(\"h-32 px-4 text-center text-sm text-muted-foreground\", className)} {...props} />\r\n    </tr>\r\n  )\r\n}\r\n\r\nexport {\r\n  Table,\r\n  TableHeader,\r\n  TableBody,\r\n  TableFooter,\r\n  TableCaption,\r\n  TableRow,\r\n  TableHead,\r\n  TableSortHead,\r\n  TableCell,\r\n  TableEmpty,\r\n  useTableSort,\r\n  useTableSelection,\r\n  sortRows,\r\n  nextSort,\r\n  type TableProps,\r\n  type TableRowProps,\r\n  type TableHeadProps,\r\n  type TableSortHeadProps,\r\n  type TableCellProps,\r\n  type TableVariant,\r\n  type TableEffect,\r\n  type TableSort,\r\n  type SortAccessor,\r\n  type SortDirection,\r\n}\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/table-head.tsx",
      "content": "import type * as React from \"react\"\nimport { useContext } from \"react\"\nimport { ChevronDown, ChevronsUpDown, ChevronUp } from \"lucide-react\"\nimport { TableContext, type TableVariant } from \"@/registry/miseui/ui/table-context\"\nimport type { SortDirection } from \"@/registry/miseui/ui/table-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst HEAD_CLASS: Record<TableVariant, string> = {\n  plain: \"h-10 border-b px-3\",\n  striped: \"h-10 px-3\",\n  card: \"h-10 border-b bg-muted px-4\",\n  compact: \"h-8 border-b px-2 pointer-coarse:h-10\",\n}\n\n/** With the button's own 6 px this adds up to the variant's cell padding, so a sortable label lines up with its column. */\nconst SORT_HEAD_PADDING: Record<TableVariant, string> = {\n  plain: \"px-1.5\",\n  striped: \"px-1.5\",\n  card: \"px-2.5\",\n  compact: \"px-0.5\",\n}\n\ntype TableHeadProps = React.ComponentProps<\"th\"> & {\n  /** Right-aligns the head over a column of figures. */\n  numeric?: boolean\n}\n\nfunction TableHead({ numeric = false, className, ...props }: TableHeadProps) {\n  const { variant, sticky } = useContext(TableContext)\n  return (\n    <th\n      scope=\"col\"\n      data-slot=\"table-head\"\n      className={cn(\n        \"text-[13px] font-medium whitespace-nowrap text-muted-foreground\",\n        numeric ? \"text-right\" : \"text-left\",\n        HEAD_CLASS[variant],\n        sticky && cn(\"sticky top-0 z-10\", variant === \"card\" ? \"bg-muted\" : \"bg-background\"),\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction SortIcon({ direction }: { direction: SortDirection | \"none\" }) {\n  if (direction === \"ascending\") return <ChevronUp aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n  if (direction === \"descending\") return <ChevronDown aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n  // Keeps its place while hidden, so the label does not shift when the column gets sorted.\n  return (\n    <ChevronsUpDown\n      aria-hidden=\"true\"\n      className=\"size-3.5 shrink-0 opacity-0 transition-opacity duration-(--duration-fast) group-hover/sort:opacity-100 group-focus-visible/sort:opacity-100 pointer-coarse:opacity-60\"\n    />\n  )\n}\n\ntype TableSortHeadProps = Omit<TableHeadProps, \"onClick\"> & {\n  direction?: SortDirection | \"none\"\n  onSort?: () => void\n}\n\nfunction TableSortHead({ direction = \"none\", onSort, numeric = false, className, children, ...props }: TableSortHeadProps) {\n  const { variant } = useContext(TableContext)\n  const icon = <SortIcon direction={direction} />\n  return (\n    <TableHead numeric={numeric} aria-sort={direction} className={cn(SORT_HEAD_PADDING[variant], className)} {...props}>\n      <button\n        type=\"button\"\n        data-slot=\"table-sort\"\n        className={cn(\n          \"group/sort inline-flex min-h-8 max-w-full items-center gap-1 rounded-sm px-1.5 font-medium outline-none pointer-coarse:min-h-10\",\n          \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:text-foreground\",\n          \"focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          direction !== \"none\" && \"text-foreground\",\n        )}\n        onClick={onSort}\n      >\n        {/* On a right-aligned column the icon goes first, so the label stays flush above the figures. */}\n        {numeric ? icon : null}\n        {children}\n        {numeric ? null : icon}\n      </button>\n    </TableHead>\n  )\n}\n\nexport { TableHead, TableSortHead, type TableHeadProps, type TableSortHeadProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/table-highlight.tsx",
      "content": "import type * as React from \"react\"\nimport { useState } from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\n\nconst ROW_SELECTOR = \"tbody tr:not([data-slot='table-empty'])\"\n\ninterface HighlightState {\n  y: number\n  height: number\n  width: number\n  visible: boolean\n  /** The first row after a pause gets no slide, or the band would fly in from wherever it last stood. */\n  arriving: boolean\n}\n\nconst HIDDEN: HighlightState = { y: 0, height: 0, width: 0, visible: false, arriving: true }\n\n/** Measured against the scroller, so a `<div>` never has to sit inside the `<table>`. */\nfunction measure(row: Element, scroller: HTMLElement) {\n  const rowRect = row.getBoundingClientRect()\n  const scrollerRect = scroller.getBoundingClientRect()\n  return { y: rowRect.top - scrollerRect.top + scroller.scrollTop, height: rowRect.height, width: scroller.scrollWidth }\n}\n\nfunction useRowHighlight(enabled: boolean) {\n  const [state, setState] = useState<HighlightState>(HIDDEN)\n  if (!enabled) return { state, handlers: undefined }\n  const moveTo = (target: EventTarget, scroller: HTMLElement) => {\n    const row = target instanceof Element ? target.closest(ROW_SELECTOR) : null\n    if (!row) return\n    const next = measure(row, scroller)\n    setState((current) => {\n      if (current.visible && current.y === next.y && current.height === next.height) return current\n      return { ...next, visible: true, arriving: !current.visible }\n    })\n  }\n  const hide = () => setState((current) => (current.visible ? { ...current, visible: false } : current))\n  const handlers = {\n    onPointerOver: (event: React.PointerEvent<HTMLDivElement>) => {\n      // A finger has no hover: the band would stick to the last row tapped.\n      if (event.pointerType !== \"touch\") moveTo(event.target, event.currentTarget)\n    },\n    onPointerLeave: hide,\n    onFocus: (event: React.FocusEvent<HTMLDivElement>) => moveTo(event.target, event.currentTarget),\n    onBlur: (event: React.FocusEvent<HTMLDivElement>) => {\n      if (!event.currentTarget.contains(event.relatedTarget)) hide()\n    },\n  }\n  return { state, handlers }\n}\n\nfunction RowHighlight({ state }: { state: HighlightState }) {\n  const reduceMotion = useReducedMotion() ?? false\n  const slide = state.arriving || reduceMotion ? { duration: 0 } : SPRING_DEFAULT\n  return (\n    <motion.div\n      aria-hidden=\"true\"\n      data-slot=\"table-highlight\"\n      data-visible={state.visible ? \"true\" : undefined}\n      className=\"pointer-events-none absolute top-0 left-0 z-0 min-w-full bg-accent\"\n      style={{ width: state.width }}\n      initial={false}\n      animate={{ y: state.y, height: state.height, opacity: state.visible ? 1 : 0 }}\n      transition={{ y: slide, height: slide, opacity: SPRING_DEFAULT }}\n    />\n  )\n}\n\nexport { RowHighlight, useRowHighlight }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/table-context.ts",
      "content": "import { createContext } from \"react\"\n\ntype TableVariant = \"plain\" | \"striped\" | \"card\" | \"compact\"\n\ninterface TableContextValue {\n  variant: TableVariant\n  sticky: boolean\n  spring: boolean\n}\n\nconst TableContext = createContext<TableContextValue>({ variant: \"plain\", sticky: false, spring: false })\n\nexport { TableContext, type TableVariant }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/table-state.ts",
      "content": "import { useMemo, useState } from \"react\"\n\ntype SortDirection = \"ascending\" | \"descending\"\n\ninterface TableSort<Key extends string> {\n  key: Key\n  direction: SortDirection\n}\n\ntype SortAccessor<Row> = (row: Row) => string | number | Date | null | undefined\n\n/** Empty cells sink to the bottom in both directions: nobody sorts to look at blanks. */\nfunction compareValues(a: unknown, b: unknown, collator: Intl.Collator) {\n  if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n  if (typeof a === \"number\" && typeof b === \"number\") return a - b\n  return collator.compare(String(a), String(b))\n}\n\n/** Numeric collation so \"Item 10\" follows \"Item 9\". A new array, and equal rows keep their order. */\nfunction sortRows<Row>(rows: readonly Row[], accessor: SortAccessor<Row>, direction: SortDirection, locale?: string) {\n  const collator = new Intl.Collator(locale, { numeric: true, sensitivity: \"base\" })\n  const sign = direction === \"ascending\" ? 1 : -1\n  const isEmpty = (value: unknown) => value === null || value === undefined || value === \"\"\n  return [...rows].sort((rowA, rowB) => {\n    const [a, b] = [accessor(rowA), accessor(rowB)]\n    if (isEmpty(a) || isEmpty(b)) return Number(isEmpty(a)) - Number(isEmpty(b))\n    return sign * compareValues(a, b, collator)\n  })\n}\n\n/** Ascending, then descending, then back to the data's own order. */\nfunction nextSort<Key extends string>(current: TableSort<Key> | null, key: Key): TableSort<Key> | null {\n  if (current?.key !== key) return { key, direction: \"ascending\" }\n  return current.direction === \"ascending\" ? { key, direction: \"descending\" } : null\n}\n\nfunction useTableSort<Row, Key extends string>(\n  rows: readonly Row[],\n  accessors: Record<Key, SortAccessor<Row>>,\n  options: { initial?: TableSort<Key> | null; locale?: string } = {},\n) {\n  const { initial = null, locale } = options\n  const [sort, setSort] = useState<TableSort<Key> | null>(initial)\n  const sorted = useMemo(\n    () => (sort ? sortRows(rows, accessors[sort.key], sort.direction, locale) : rows),\n    [rows, accessors, sort, locale],\n  )\n  const headProps = (key: Key) => ({\n    direction: sort?.key === key ? sort.direction : (\"none\" as const),\n    onSort: () => setSort((current) => nextSort(current, key)),\n  })\n  return { sorted, sort, setSort, headProps }\n}\n\ninterface TableSelectionOptions<Id> {\n  /** Every selectable row on the screen, in display order. */\n  ids: readonly Id[]\n  defaultSelected?: readonly Id[]\n}\n\n/** Row selection state. Hand `checked` to a Checkbox: it is \"indeterminate\" when only some rows are chosen. */\nfunction useTableSelection<Id>({ ids, defaultSelected = [] }: TableSelectionOptions<Id>) {\n  const [selected, setSelected] = useState<ReadonlySet<Id>>(() => new Set(defaultSelected))\n  const onScreen = ids.filter((id) => selected.has(id))\n  const toggle = (id: Id) =>\n    setSelected((current) => {\n      const next = new Set(current)\n      if (!next.delete(id)) next.add(id)\n      return next\n    })\n  const allOn = ids.length > 0 && onScreen.length === ids.length\n  const toggleAll = () =>\n    setSelected((current) => {\n      const next = new Set(current)\n      ids.forEach((id) => (allOn ? next.delete(id) : next.add(id)))\n      return next\n    })\n  return {\n    selected,\n    count: onScreen.length,\n    isSelected: (id: Id) => selected.has(id),\n    toggle,\n    toggleAll,\n    clear: () => setSelected(new Set()),\n    checked: allOn ? true : onScreen.length > 0 ? (\"indeterminate\" as const) : false,\n  }\n}\n\nexport { nextSort, sortRows, useTableSelection, useTableSort, type SortAccessor, type SortDirection, type TableSort }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}