{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-sort",
  "type": "registry:block",
  "title": "Sortable Table",
  "description": "Searchable and sortable name, email and company table with an empty state.",
  "dependencies": [
    "@mantine/core@^9",
    "@tabler/icons-react@^3"
  ],
  "docs": "Adapted from Mantine UI TableSort at ffbf61c559f374a7ea28fcf00355e84dcbe9a908; accepts consumer data and derives filtered rows without duplicating prop state.",
  "registryDependencies": [
    "@house/mantine-ui-license"
  ],
  "files": [
    {
      "path": "registry/mantine-ui/table-sort/table-sort.tsx",
      "type": "registry:ui",
      "content": "/**\n * Adapted from Mantine UI's TableSort at\n * ffbf61c559f374a7ea28fcf00355e84dcbe9a908. MIT licensed; see\n * LICENSES/MANTINE-UI.txt after installation.\n */\n\nimport { Center, Group, ScrollArea, Table, Text, TextInput, UnstyledButton } from \"@mantine/core\";\nimport { IconChevronDown, IconChevronUp, IconSearch, IconSelector } from \"@tabler/icons-react\";\nimport type { ChangeEvent, ReactNode } from \"react\";\nimport { useMemo, useState } from \"react\";\n\nimport classes from \"./table-sort.module.css\";\n\nexport interface TableSortRow {\n  name: string;\n  email: string;\n  company: string;\n}\n\nexport interface TableSortProps {\n  data: TableSortRow[];\n  searchPlaceholder?: string;\n  emptyMessage?: ReactNode;\n  onRowClick?: (row: TableSortRow) => void;\n}\n\ninterface SortableHeaderProps {\n  children: ReactNode;\n  reversed: boolean;\n  sorted: boolean;\n  onSort: () => void;\n}\n\nfunction SortableHeader({ children, reversed, sorted, onSort }: SortableHeaderProps) {\n  const Icon = sorted ? (reversed ? IconChevronUp : IconChevronDown) : IconSelector;\n\n  return (\n    <Table.Th\n      className={classes.th}\n      aria-sort={sorted ? (reversed ? \"descending\" : \"ascending\") : \"none\"}\n    >\n      <UnstyledButton onClick={onSort} className={classes.control}>\n        <Group justify=\"space-between\">\n          <Text fw={500} fz=\"sm\">\n            {children}\n          </Text>\n          <Center className={classes.icon}>\n            <Icon size={16} stroke={1.5} />\n          </Center>\n        </Group>\n      </UnstyledButton>\n    </Table.Th>\n  );\n}\n\nfunction filterData(data: TableSortRow[], search: string): TableSortRow[] {\n  const query = search.toLowerCase().trim();\n  if (!query) return data;\n\n  return data.filter((row) =>\n    [row.name, row.email, row.company].some((value) => value.toLowerCase().includes(query)),\n  );\n}\n\nfunction sortData(\n  data: TableSortRow[],\n  sortBy: keyof TableSortRow | null,\n  reversed: boolean,\n  search: string,\n): TableSortRow[] {\n  const filtered = filterData(data, search);\n  if (!sortBy) return filtered;\n\n  return [...filtered].sort((left, right) => {\n    const result = left[sortBy].localeCompare(right[sortBy]);\n    return reversed ? -result : result;\n  });\n}\n\nexport function TableSort({\n  data,\n  searchPlaceholder = \"Search by any field\",\n  emptyMessage = \"Nothing found\",\n  onRowClick,\n}: TableSortProps) {\n  const [search, setSearch] = useState(\"\");\n  const [sortBy, setSortBy] = useState<keyof TableSortRow | null>(null);\n  const [reversed, setReversed] = useState(false);\n  const sortedData = useMemo(\n    () => sortData(data, sortBy, reversed, search),\n    [data, reversed, search, sortBy],\n  );\n\n  const setSorting = (field: keyof TableSortRow) => {\n    const nextReversed = field === sortBy ? !reversed : false;\n    setReversed(nextReversed);\n    setSortBy(field);\n  };\n\n  const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {\n    setSearch(event.currentTarget.value);\n  };\n\n  const rows = sortedData.map((row) => (\n    <Table.Tr\n      key={`${row.email}\\u0000${row.name}`}\n      onClick={onRowClick ? () => onRowClick(row) : undefined}\n      className={onRowClick ? classes.clickableRow : undefined}\n    >\n      <Table.Td>{row.name}</Table.Td>\n      <Table.Td>{row.email}</Table.Td>\n      <Table.Td>{row.company}</Table.Td>\n    </Table.Tr>\n  ));\n\n  return (\n    <ScrollArea>\n      <TextInput\n        placeholder={searchPlaceholder}\n        mb=\"md\"\n        leftSection={<IconSearch size={16} stroke={1.5} />}\n        value={search}\n        onChange={handleSearchChange}\n      />\n      <Table horizontalSpacing=\"md\" verticalSpacing=\"xs\" miw={560} layout=\"fixed\">\n        <Table.Thead>\n          <Table.Tr>\n            <SortableHeader\n              sorted={sortBy === \"name\"}\n              reversed={reversed}\n              onSort={() => setSorting(\"name\")}\n            >\n              Name\n            </SortableHeader>\n            <SortableHeader\n              sorted={sortBy === \"email\"}\n              reversed={reversed}\n              onSort={() => setSorting(\"email\")}\n            >\n              Email\n            </SortableHeader>\n            <SortableHeader\n              sorted={sortBy === \"company\"}\n              reversed={reversed}\n              onSort={() => setSorting(\"company\")}\n            >\n              Company\n            </SortableHeader>\n          </Table.Tr>\n        </Table.Thead>\n        <Table.Tbody>\n          {rows.length > 0 ? (\n            rows\n          ) : (\n            <Table.Tr>\n              <Table.Td colSpan={3}>\n                <Text fw={500} ta=\"center\">\n                  {emptyMessage}\n                </Text>\n              </Table.Td>\n            </Table.Tr>\n          )}\n        </Table.Tbody>\n      </Table>\n    </ScrollArea>\n  );\n}\n"
    },
    {
      "path": "registry/mantine-ui/table-sort/table-sort.module.css",
      "type": "registry:file",
      "target": "@ui/table-sort.module.css",
      "content": "/*\n * Adapted from Mantine UI's TableSort at\n * ffbf61c559f374a7ea28fcf00355e84dcbe9a908. MIT licensed; see\n * LICENSES/MANTINE-UI.txt after installation.\n */\n.th {\n  padding: 0;\n}\n\n.control {\n  width: 100%;\n  padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);\n}\n\n.control:hover {\n  background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));\n}\n\n.icon {\n  width: 21px;\n  height: 21px;\n  border-radius: 21px;\n}\n\n.clickableRow {\n  cursor: pointer;\n}\n"
    }
  ],
  "meta": {
    "mantine": {
      "requires": ">=9",
      "provider": "MantineProvider",
      "props": {
        "TableSort": [
          {
            "name": "data",
            "type": "TableSortRow[]",
            "required": true,
            "description": "Rows to display, searched and sorted in place before rendering."
          },
          {
            "name": "searchPlaceholder",
            "type": "string",
            "required": false,
            "default": "\"Search by any field\"",
            "description": "Placeholder text for the search input above the table."
          },
          {
            "name": "emptyMessage",
            "type": "ReactNode",
            "required": false,
            "default": "\"Nothing found\"",
            "description": "Message rendered in a full-width row when the filtered data is empty."
          },
          {
            "name": "onRowClick",
            "type": "(row: TableSortRow) => void",
            "required": false,
            "description": "Called with the row data when a table row is clicked; rows become clickable only when this is provided."
          }
        ]
      },
      "usage": {
        "path": "registry/mantine-ui/table-sort/table-sort.usage.tsx",
        "content": "import { TableSort } from \"@/components/ui/table-sort\";\n\nexport function TeamDirectoryTable() {\n  return (\n    <TableSort\n      data={[\n        { name: \"Robert Wolfkisser\", email: \"rob_wolf@gmail.com\", company: \"Canyon Realty\" },\n        { name: \"Jill Jailbreaker\", email: \"jj@breaker.com\", company: \"Fishing Corp\" },\n        { name: \"Henry Silkeater\", email: \"henry@silkeater.io\", company: \"Wool Charts\" },\n        { name: \"Bill Horsefighter\", email: \"bhorsefighter@royal.net\", company: \"Combat Farms\" },\n        { name: \"Jeremy Footviewer\", email: \"jeremy@foot.dev\", company: \"Footwork Inc\" },\n      ]}\n      searchPlaceholder=\"Search team members\"\n      onRowClick={(row) => console.log(\"row clicked\", row)}\n    />\n  );\n}\n"
      }
    }
  }
}
