{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table",
  "title": "Data Table",
  "description": "TanStack Table wrapper with toolbar, faceted filters, pagination, column visibility and resizing.",
  "dependencies": [
    "@tanstack/react-table@^8.21.3",
    "lucide-react@^1.48.0"
  ],
  "registryDependencies": [
    "https://rdf.dev.360online.com/r/badge.json",
    "https://rdf.dev.360online.com/r/button.json",
    "https://rdf.dev.360online.com/r/command.json",
    "https://rdf.dev.360online.com/r/dropdown-menu.json",
    "https://rdf.dev.360online.com/r/input.json",
    "https://rdf.dev.360online.com/r/pagination.json",
    "https://rdf.dev.360online.com/r/popover.json",
    "https://rdf.dev.360online.com/r/separator.json",
    "https://rdf.dev.360online.com/r/table.json",
    "https://rdf.dev.360online.com/r/tooltip.json"
  ],
  "files": [
    {
      "path": "registry/rapid/ui/data-table/creator-name-cell.tsx",
      "content": "'use client'\n\ninterface Creator {\n\tid: string\n\tname: string\n\temail?: string\n}\n\ninterface CreatorNameCellProps {\n\tcreator?: Creator | null\n}\n\nexport function CreatorNameCell({ creator }: CreatorNameCellProps) {\n\treturn <span>{creator?.name || '-'}</span>\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/creator-name-cell.tsx"
    },
    {
      "path": "registry/rapid/ui/data-table/data-table-column-header.tsx",
      "content": "import type { Column, SortDirection } from '@tanstack/react-table'\nimport { ArrowDownIcon, ArrowUpIcon } from 'lucide-react'\n\nimport { cn } from '@/lib/utils'\n\ninterface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {\n\tcolumn: Column<TData, TValue>\n\ttitle: string\n}\n\nfunction SortIcon({ direction }: Readonly<{ direction: false | SortDirection }>) {\n\tif (direction === 'desc') {\n\t\treturn <ArrowDownIcon className=\"ml-1 size-4 shrink-0\" aria-hidden=\"true\" focusable=\"false\" />\n\t}\n\tif (direction === 'asc') {\n\t\treturn <ArrowUpIcon className=\"ml-1 size-4 shrink-0\" aria-hidden=\"true\" focusable=\"false\" />\n\t}\n\treturn null\n}\n\nexport function DataTableColumnHeader<TData, TValue>({\n\tcolumn,\n\ttitle,\n\tclassName,\n}: DataTableColumnHeaderProps<TData, TValue>) {\n\tif (!column.getCanSort()) {\n\t\treturn (\n\t\t\t<div className={cn('flex text-sm font-medium items-center text-left min-w-0', className)}>\n\t\t\t\t<span className=\"truncate\">{title}</span>\n\t\t\t</div>\n\t\t)\n\t}\n\n\tfunction handleSort() {\n\t\tif (column.getIsSorted() === false) {\n\t\t\tcolumn.toggleSorting(false) // Set to asc\n\t\t} else if (column.getIsSorted() === 'asc') {\n\t\t\tcolumn.toggleSorting(true) // Set to desc\n\t\t} else {\n\t\t\tcolumn.clearSorting() // Clear sorting\n\t\t}\n\t}\n\n\tconst sortDirection = column.getIsSorted()\n\tconst sortLabels: Record<string, string> = { asc: 'sorted ascending', desc: 'sorted descending' }\n\tconst sortLabel = sortLabels[sortDirection as string] ?? 'not sorted'\n\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tclassName={cn(\n\t\t\t\t'flex w-full min-w-0 cursor-pointer items-center rounded-sm text-sm font-medium outline-none hover:text-foreground/80 focus-visible:ring-3 focus-visible:ring-ring',\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\tonClick={handleSort}\n\t\t\taria-label={`${title}, ${sortLabel}`}\n\t\t\tdata-state={sortDirection ? 'sorted' : 'unsorted'}\n\t\t>\n\t\t\t<span className=\"truncate\">{title}</span>\n\t\t\t{/* Only reserve icon space when actually sorted — an always-on placeholder squeezes narrow\n\t\t\t    columns (e.g. Version) and forces the header to truncate unnecessarily. */}\n\t\t\t<SortIcon direction={sortDirection} />\n\t\t</button>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/data-table-column-header.tsx"
    },
    {
      "path": "registry/rapid/ui/data-table/data-table-faceted-filter.tsx",
      "content": "import type { Column } from '@tanstack/react-table'\nimport { CheckIcon, CirclePlusIcon } from 'lucide-react'\nimport type * as React from 'react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport {\n\tCommand,\n\tCommandEmpty,\n\tCommandGroup,\n\tCommandInput,\n\tCommandItem,\n\tCommandList,\n\tCommandSeparator,\n} from '@/components/ui/command'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { Separator } from '@/components/ui/separator'\nimport { cn } from '@/lib/utils'\n\ninterface DataTableFacetedFilterProps<TData, TValue> {\n\tcolumn?: Column<TData, TValue>\n\ttitle?: string\n\toptions: {\n\t\tlabel: string\n\t\tvalue: string\n\t\ticon?: React.ComponentType<{ className?: string }>\n\t}[]\n}\n\nexport function DataTableFacetedFilter<TData, TValue>({\n\tcolumn,\n\ttitle,\n\toptions,\n}: DataTableFacetedFilterProps<TData, TValue>) {\n\tconst facets = column?.getFacetedUniqueValues()\n\tconst selectedValues = new Set(column?.getFilterValue() as string[])\n\n\treturn (\n\t\t<Popover>\n\t\t\t<PopoverTrigger render={<Button variant=\"outline\" className=\"border-dashed\" />}>\n\t\t\t\t<CirclePlusIcon data-icon=\"inline-start\" />\n\t\t\t\t{title}\n\t\t\t\t{selectedValues?.size > 0 && (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<Separator orientation=\"vertical\" className=\"mx-0.5 h-4\" />\n\t\t\t\t\t\t<Badge className=\"rounded-sm px-1 font-normal lg:hidden\">{selectedValues.size}</Badge>\n\t\t\t\t\t\t<div className=\"hidden gap-1 lg:flex\">\n\t\t\t\t\t\t\t{selectedValues.size > 2 ? (\n\t\t\t\t\t\t\t\t<Badge className=\"rounded-sm px-1 font-normal\">{selectedValues.size} selected</Badge>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\toptions\n\t\t\t\t\t\t\t\t\t.filter((option) => selectedValues.has(option.value))\n\t\t\t\t\t\t\t\t\t.map((option) => (\n\t\t\t\t\t\t\t\t\t\t<Badge key={option.value} className=\"rounded-sm px-1 font-normal\">\n\t\t\t\t\t\t\t\t\t\t\t{option.label}\n\t\t\t\t\t\t\t\t\t\t</Badge>\n\t\t\t\t\t\t\t\t\t))\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</>\n\t\t\t\t)}\n\t\t\t</PopoverTrigger>\n\t\t\t<PopoverContent className=\"w-[200px] p-0\" align=\"start\">\n\t\t\t\t<Command>\n\t\t\t\t\t<CommandInput placeholder={title} />\n\t\t\t\t\t<CommandList>\n\t\t\t\t\t\t<CommandEmpty>No results found.</CommandEmpty>\n\t\t\t\t\t\t<CommandGroup>\n\t\t\t\t\t\t\t{options.map((option) => {\n\t\t\t\t\t\t\t\tconst isSelected = selectedValues.has(option.value)\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<CommandItem\n\t\t\t\t\t\t\t\t\t\tkey={option.value}\n\t\t\t\t\t\t\t\t\t\tonSelect={() => {\n\t\t\t\t\t\t\t\t\t\t\tif (isSelected) {\n\t\t\t\t\t\t\t\t\t\t\t\tselectedValues.delete(option.value)\n\t\t\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\t\t\tselectedValues.add(option.value)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tconst filterValues = Array.from(selectedValues)\n\t\t\t\t\t\t\t\t\t\t\tcolumn?.setFilterValue(filterValues.length ? filterValues : undefined)\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t'flex size-4 items-center justify-center rounded-[4px] border border-primary',\n\t\t\t\t\t\t\t\t\t\t\t\tisSelected ? 'bg-primary text-primary-foreground' : 'opacity-50 [&_svg]:invisible',\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<CheckIcon className=\"size-3.5 text-current\" />\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t{option.icon && <option.icon className=\"size-4 text-muted-foreground\" />}\n\t\t\t\t\t\t\t\t\t\t<span>{option.label}</span>\n\t\t\t\t\t\t\t\t\t\t{facets?.get(option.value) && (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"ml-auto flex size-4 items-center justify-center font-mono text-xs\">\n\t\t\t\t\t\t\t\t\t\t\t\t{facets.get(option.value)}\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t</CommandGroup>\n\t\t\t\t\t\t{selectedValues.size > 0 && (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<CommandSeparator />\n\t\t\t\t\t\t\t\t<CommandGroup>\n\t\t\t\t\t\t\t\t\t<CommandItem onSelect={() => column?.setFilterValue(undefined)} className=\"justify-center text-center\">\n\t\t\t\t\t\t\t\t\t\tClear filters\n\t\t\t\t\t\t\t\t\t</CommandItem>\n\t\t\t\t\t\t\t\t</CommandGroup>\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</CommandList>\n\t\t\t\t</Command>\n\t\t\t</PopoverContent>\n\t\t</Popover>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/data-table-faceted-filter.tsx"
    },
    {
      "path": "registry/rapid/ui/data-table/data-table-pagination.tsx",
      "content": "'use client'\n\nimport type { Table } from '@tanstack/react-table'\nimport * as React from 'react'\nimport {\n\tPagination,\n\tPaginationContent,\n\tPaginationEllipsis,\n\tPaginationItem,\n\tPaginationLink,\n\tPaginationNext,\n\tPaginationPrevious,\n} from '@/components/ui/pagination'\n\n// Pagination links are anchors, which have no `disabled` attribute.\nconst DISABLED_LINK_PROPS = { 'aria-disabled': true, tabIndex: -1, className: 'pointer-events-none opacity-50' }\n\nfunction previousLinkProps<TData>(table: Table<TData>) {\n\treturn table.getCanPreviousPage() ? {} : DISABLED_LINK_PROPS\n}\n\nfunction nextLinkProps<TData>(table: Table<TData>) {\n\treturn table.getCanNextPage() ? {} : DISABLED_LINK_PROPS\n}\n\ninterface DataTablePaginationProps<TData> {\n\ttable: Table<TData>\n\tonPaginationChange?: (values: { pageIndex: number; pageSize: number }) => void\n}\n\nexport function DataTablePagination<TData>({ table, onPaginationChange }: DataTablePaginationProps<TData>) {\n\tconst handlePageChange = React.useCallback(\n\t\t(newPageIndex: number) => {\n\t\t\tonPaginationChange?.({\n\t\t\t\tpageIndex: newPageIndex,\n\t\t\t\tpageSize: table.getState().pagination.pageSize,\n\t\t\t})\n\t\t},\n\t\t[onPaginationChange, table],\n\t)\n\n\t// Read current page from the table's controlled state\n\tconst currentPageIndex = table.getState().pagination.pageIndex ?? 0\n\n\tconst getPageNumbers = () => {\n\t\tconst totalPages = table.getPageCount()\n\t\tconst pages: (number | string)[] = []\n\n\t\tif (totalPages <= 7) {\n\t\t\treturn Array.from({ length: totalPages }, (_, i) => i)\n\t\t}\n\n\t\tpages.push(0)\n\n\t\tconst middleStart = Math.max(1, Math.min(currentPageIndex - 1, totalPages - 5))\n\t\tconst middleEnd = Math.min(middleStart + 3, totalPages - 2)\n\n\t\tif (middleStart > 1) pages.push('ellipsis')\n\n\t\tfor (let i = middleStart; i <= middleEnd; i++) {\n\t\t\tpages.push(i)\n\t\t}\n\n\t\tif (middleEnd < totalPages - 2) pages.push('ellipsis')\n\t\tif (totalPages > 1) pages.push(totalPages - 1)\n\n\t\treturn pages\n\t}\n\n\treturn (\n\t\t<div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n\t\t\t<Pagination>\n\t\t\t\t<PaginationContent>\n\t\t\t\t\t<PaginationItem>\n\t\t\t\t\t\t<PaginationPrevious\n\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t{...previousLinkProps(table)}\n\t\t\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\t\t\tif (table.getCanPreviousPage()) handlePageChange(currentPageIndex - 1)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</PaginationItem>\n\n\t\t\t\t\t{getPageNumbers().map((pageNum, idx) =>\n\t\t\t\t\t\tpageNum === 'ellipsis' ? (\n\t\t\t\t\t\t\t<PaginationItem\n\t\t\t\t\t\t\t\tkey={`ellipsis-${\n\t\t\t\t\t\t\t\t\t// biome-ignore lint/suspicious/noArrayIndexKey: ellipsis items need index-based keys\n\t\t\t\t\t\t\t\t\tidx\n\t\t\t\t\t\t\t\t}`}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<PaginationEllipsis />\n\t\t\t\t\t\t\t</PaginationItem>\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<PaginationItem key={pageNum}>\n\t\t\t\t\t\t\t\t<PaginationLink\n\t\t\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\t\t\t\t\thandlePageChange(pageNum as number)\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tisActive={currentPageIndex === pageNum}\n\t\t\t\t\t\t\t\t\ttotal={table.getPageCount()}\n\t\t\t\t\t\t\t\t\taria-label={`Go to page ${(pageNum as number) + 1}`}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{(pageNum as number) + 1}\n\t\t\t\t\t\t\t\t</PaginationLink>\n\t\t\t\t\t\t\t</PaginationItem>\n\t\t\t\t\t\t),\n\t\t\t\t\t)}\n\n\t\t\t\t\t<PaginationItem>\n\t\t\t\t\t\t<PaginationNext\n\t\t\t\t\t\t\thref=\"#\"\n\t\t\t\t\t\t\t{...nextLinkProps(table)}\n\t\t\t\t\t\t\tonClick={(event) => {\n\t\t\t\t\t\t\t\tevent.preventDefault()\n\t\t\t\t\t\t\t\tif (table.getCanNextPage()) handlePageChange(currentPageIndex + 1)\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</PaginationItem>\n\t\t\t\t</PaginationContent>\n\t\t\t</Pagination>\n\t\t</div>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/data-table-pagination.tsx"
    },
    {
      "path": "registry/rapid/ui/data-table/data-table-toolbar.tsx",
      "content": "import type { Table } from '@tanstack/react-table'\nimport { XIcon } from 'lucide-react'\n\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { statuses } from './data'\nimport { DataTableFacetedFilter } from './data-table-faceted-filter'\nimport { DataTableViewOptions } from './data-table-view-options'\n\ninterface DataTableToolbarProps<TData> {\n\ttable: Table<TData>\n}\n\nexport function DataTableToolbar<TData>({ table }: DataTableToolbarProps<TData>) {\n\tconst isFiltered = table.getState().columnFilters.length > 0\n\n\treturn (\n\t\t<div className=\"flex items-center justify-between\">\n\t\t\t<div className=\"flex flex-1 items-center gap-2\">\n\t\t\t\t<Input\n\t\t\t\t\tplaceholder=\"Filter cases...\"\n\t\t\t\t\tvalue={(table.getColumn('name')?.getFilterValue() as string) ?? ''}\n\t\t\t\t\tonChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}\n\t\t\t\t\tclassName=\"w-[150px] lg:w-[250px]\"\n\t\t\t\t/>\n\t\t\t\t{table.getColumn('status') && (\n\t\t\t\t\t<DataTableFacetedFilter column={table.getColumn('status')} title=\"Status\" options={statuses} />\n\t\t\t\t)}\n\t\t\t\t{/* {table.getColumn('priority') && (\n\t\t\t\t\t<DataTableFacetedFilter column={table.getColumn('priority')} title=\"Priority\" options={priorities} />\n\t\t\t\t)} */}\n\t\t\t\t{isFiltered && (\n\t\t\t\t\t<Button variant=\"ghost\" onClick={() => table.resetColumnFilters()}>\n\t\t\t\t\t\tReset\n\t\t\t\t\t\t<XIcon data-icon=\"inline-end\" />\n\t\t\t\t\t</Button>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t<DataTableViewOptions table={table} />\n\t\t</div>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/data-table-toolbar.tsx"
    },
    {
      "path": "registry/rapid/ui/data-table/data-table-view-options.tsx",
      "content": "import type { Table } from '@tanstack/react-table'\nimport { Settings2Icon } from 'lucide-react'\n\nimport { Button } from '@/components/ui/button'\nimport {\n\tDropdownMenu,\n\tDropdownMenuCheckboxItem,\n\tDropdownMenuContent,\n\tDropdownMenuGroup,\n\tDropdownMenuLabel,\n\tDropdownMenuSeparator,\n\tDropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\n\ninterface DataTableViewOptionsProps<TData> {\n\ttable: Table<TData>\n}\n\nexport function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {\n\treturn (\n\t\t<DropdownMenu>\n\t\t\t<DropdownMenuTrigger render={<Button variant=\"outline\" className=\"ml-auto hidden lg:flex\" />}>\n\t\t\t\t<Settings2Icon data-icon=\"inline-start\" />\n\t\t\t\tView\n\t\t\t</DropdownMenuTrigger>\n\t\t\t<DropdownMenuContent align=\"end\" className=\"w-[150px]\">\n\t\t\t\t<DropdownMenuGroup>\n\t\t\t\t\t<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>\n\t\t\t\t</DropdownMenuGroup>\n\t\t\t\t<DropdownMenuSeparator />\n\t\t\t\t{table\n\t\t\t\t\t.getAllColumns()\n\t\t\t\t\t.filter((column) => typeof column.accessorFn !== 'undefined' && column.getCanHide())\n\t\t\t\t\t.map((column) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<DropdownMenuCheckboxItem\n\t\t\t\t\t\t\t\tkey={column.id}\n\t\t\t\t\t\t\t\tclassName=\"capitalize\"\n\t\t\t\t\t\t\t\tchecked={column.getIsVisible()}\n\t\t\t\t\t\t\t\tonCheckedChange={(value) => column.toggleVisibility(!!value)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{column.id}\n\t\t\t\t\t\t\t</DropdownMenuCheckboxItem>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t</DropdownMenuContent>\n\t\t</DropdownMenu>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/data-table-view-options.tsx"
    },
    {
      "path": "registry/rapid/ui/data-table/data-table.tsx",
      "content": "'use client'\n\nimport {\n\ttype ColumnDef,\n\ttype ColumnFiltersState,\n\ttype ColumnSizingState,\n\tflexRender,\n\tgetCoreRowModel,\n\tgetExpandedRowModel,\n\tgetFacetedRowModel,\n\tgetFacetedUniqueValues,\n\tgetFilteredRowModel,\n\tgetPaginationRowModel,\n\tgetSortedRowModel,\n\ttype OnChangeFn,\n\ttype PaginationState,\n\ttype RowData,\n\ttype SortingState,\n\ttype TableMeta,\n\tuseReactTable,\n\ttype VisibilityState,\n} from '@tanstack/react-table'\nimport { ChevronDownIcon, ChevronRightIcon } from 'lucide-react'\nimport * as React from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'\nimport { cn } from '@/lib/utils'\nimport { DataTablePagination } from './data-table-pagination'\n\ndeclare module '@tanstack/react-table' {\n\t// biome-ignore lint/correctness/noUnusedVariables: the generics must match TanStack's declaration to merge with it\n\tinterface ColumnMeta<TData extends RowData, TValue> {\n\t\t/** Pin this column to the right edge so it stays visible while the table scrolls horizontally. */\n\t\tpinRight?: boolean\n\t\t/** Opt out of the default single-line truncation applied to cells. */\n\t\ttruncate?: boolean\n\t}\n}\n\n// Sticky positioning for a right-pinned column. Colours are applied inline (see pinnedCellStyle),\n// because `background: inherit` won't track a row's :hover on table cells.\nconst pinRightStyle: React.CSSProperties = {\n\tposition: 'sticky',\n\tright: 0,\n}\n\n// Opaque background for a pinned cell that matches the row exactly and hides cells scrolling underneath.\n// Selected → solid muted; hovered → the same translucent muted the row uses, composited over an opaque\n// base (background-image over background-color) so it matches the row's hover shade while staying opaque.\nconst MUTED_50 = 'color-mix(in oklch, var(--muted) 50%, transparent)'\n// Opaque equivalent of `bg-muted/50` over the page background (used by the muted header and sub-rows).\nconst MUTED_SURFACE = 'color-mix(in oklch, var(--muted) 50%, var(--background))'\nfunction pinnedCellStyle(muted: boolean, active: boolean, hovered: boolean): React.CSSProperties {\n\tconst base = muted ? MUTED_SURFACE : 'var(--background)'\n\tif (active) return { ...pinRightStyle, zIndex: 1, backgroundColor: 'var(--muted)' }\n\treturn {\n\t\t...pinRightStyle,\n\t\tzIndex: 1,\n\t\tbackgroundColor: base,\n\t\tbackgroundImage: hovered ? `linear-gradient(${MUTED_50}, ${MUTED_50})` : undefined,\n\t}\n}\n\n// Width of the leading expander column when getSubRows is used (matches the 3rem cells below).\nconst EXPANDER_WIDTH = 48\n// Smallest a column can be dragged, so it never collapses past a letter + ellipsis (\"N…\").\nconst MIN_COLUMN_WIDTH = 48\n\n// Resize grab strip on a header's right edge, with a short divider line: border colour at rest, primary\n// colour (thicker) on hover/drag.\nfunction ResizeHandle({ resizeHandler, isResizing }: { resizeHandler: (e: unknown) => void; isResizing: boolean }) {\n\tconst [hovered, setHovered] = React.useState(false)\n\tconst active = hovered || isResizing\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\taria-label=\"Resize column\"\n\t\t\tonMouseDown={resizeHandler}\n\t\t\tonTouchStart={resizeHandler}\n\t\t\tonClick={(e) => e.stopPropagation()}\n\t\t\tonMouseEnter={() => setHovered(true)}\n\t\t\tonMouseLeave={() => setHovered(false)}\n\t\t\tstyle={{ cursor: 'col-resize' }}\n\t\t\tclassName=\"absolute inset-y-0 right-0 z-10 flex w-3 touch-none select-none items-stretch justify-end border-0 bg-transparent p-0\"\n\t\t>\n\t\t\t<span\n\t\t\t\tstyle={{\n\t\t\t\t\twidth: active ? 2 : 1,\n\t\t\t\t\tbackgroundColor: active ? 'var(--primary)' : 'var(--border)',\n\t\t\t\t\ttransition: 'width 0.15s, background-color 0.15s',\n\t\t\t\t}}\n\t\t\t/>\n\t\t</button>\n\t)\n}\n\ninterface DataTableProps<TData extends RowData, TValue> {\n\t/** `muted` (default) gives the header row a `bg-muted/50` surface; `default` keeps it transparent. */\n\tvariant?: 'default' | 'muted'\n\tcolumns: ColumnDef<TData, TValue>[]\n\tdata: TData[]\n\tpagination?: PaginationState\n\tpageCount?: number\n\tgetSubRows?: (row: TData) => TData[] | undefined\n\tonPaginationChange?: OnChangeFn<PaginationState>\n\tonSortingChange?: OnChangeFn<SortingState>\n\tsorting?: SortingState\n\tmanualPagination?: boolean\n\tmanualSorting?: boolean\n\tdefaultPageSize?: number\n\tmeta?: TableMeta<TData>\n\t/** Allow users to drag column borders to resize. On by default; pass false to opt a table out. */\n\tenableColumnResizing?: boolean\n}\n\nexport function DataTable<TData extends RowData, TValue>({\n\tcolumns,\n\tdata,\n\tpagination,\n\tpageCount,\n\tgetSubRows,\n\tonPaginationChange,\n\tonSortingChange,\n\tsorting: initialSorting = [],\n\tmanualPagination = false,\n\tmanualSorting = false,\n\tdefaultPageSize = 10,\n\tvariant = 'muted',\n\tmeta,\n\tenableColumnResizing = true,\n}: DataTableProps<TData, TValue>) {\n\tconst [rowSelection, setRowSelection] = React.useState({})\n\tconst [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})\n\tconst [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])\n\tconst [sorting, setSorting] = React.useState<SortingState>(initialSorting)\n\tconst [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})\n\t// Once the user drags a column, stop auto-fitting to the container so their chosen widths stick.\n\tconst [userResized, setUserResized] = React.useState(false)\n\tconst scrollRef = React.useRef<HTMLDivElement>(null)\n\n\tconst safePageSize = defaultPageSize > 0 ? defaultPageSize : 10\n\n\tconst defaultPagination: PaginationState = {\n\t\tpageIndex: 0,\n\t\tpageSize: safePageSize,\n\t}\n\n\tconst [internalPagination, setInternalPagination] = React.useState<PaginationState>(defaultPagination)\n\tconst handlePaginationChange = pagination ? onPaginationChange : setInternalPagination\n\n\tconst table = useReactTable({\n\t\tdata,\n\t\tcolumns,\n\t\tstate: {\n\t\t\tpagination: pagination ?? internalPagination,\n\t\t\tsorting,\n\t\t\tcolumnVisibility,\n\t\t\trowSelection,\n\t\t\tcolumnFilters,\n\t\t\tcolumnSizing,\n\t\t},\n\t\tenableColumnResizing,\n\t\tcolumnResizeMode: 'onChange',\n\t\tonColumnSizingChange: (updater) => {\n\t\t\t// A change coming through here is a user drag (our auto-fit sets state directly), so lock in.\n\t\t\tsetUserResized(true)\n\t\t\tsetColumnSizing(updater)\n\t\t},\n\t\t// Floor the drag width so a column never collapses to nothing — the header keeps at least\n\t\t// a letter + ellipsis (\"N…\") instead of fully disappearing.\n\t\tdefaultColumn: { minSize: MIN_COLUMN_WIDTH },\n\t\tmanualPagination,\n\t\tmanualSorting,\n\t\tpageCount,\n\t\tonPaginationChange: handlePaginationChange,\n\t\tonSortingChange: (updater) => {\n\t\t\tconst newState = typeof updater === 'function' ? updater(sorting) : updater\n\t\t\tsetSorting(newState)\n\t\t\tonSortingChange?.(newState)\n\t\t},\n\t\tenableRowSelection: true,\n\t\tonRowSelectionChange: setRowSelection,\n\t\tonColumnFiltersChange: setColumnFilters,\n\t\tonColumnVisibilityChange: setColumnVisibility,\n\t\tgetCoreRowModel: getCoreRowModel(),\n\t\tgetFilteredRowModel: getFilteredRowModel(),\n\t\tgetPaginationRowModel: getPaginationRowModel(),\n\t\tgetSortedRowModel: getSortedRowModel(),\n\t\tgetFacetedRowModel: getFacetedRowModel(),\n\t\tgetFacetedUniqueValues: getFacetedUniqueValues(),\n\t\tgetExpandedRowModel: getExpandedRowModel(),\n\t\tgetSubRows,\n\t\tmeta,\n\t})\n\n\t// Auto-fit: distribute the container width across columns (proportional to their defined sizes) so\n\t// they fill the table at rest. Stops once the user manually resizes, and re-runs on container resize.\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: table identity is stable enough here.\n\tReact.useLayoutEffect(() => {\n\t\tif (!enableColumnResizing || userResized) return\n\t\tconst el = scrollRef.current\n\t\tif (!el) return\n\t\tconst fitColumns = () => {\n\t\t\tconst leafs = table.getVisibleLeafColumns()\n\t\t\tif (!leafs.length) return\n\t\t\tconst available = el.clientWidth - (getSubRows ? EXPANDER_WIDTH : 0)\n\t\t\tconst sum = leafs.reduce((a, c) => a + c.getSize(), 0)\n\t\t\tif (available <= 0 || sum <= 0 || available <= sum) return // only grow to fill, never shrink below defined\n\t\t\tconst next: ColumnSizingState = {}\n\t\t\tfor (const c of leafs) {\n\t\t\t\tnext[c.id] = (c.getSize() / sum) * available\n\t\t\t}\n\t\t\tsetColumnSizing(next)\n\t\t}\n\t\tfitColumns()\n\t\tconst ro = new ResizeObserver(fitColumns)\n\t\tro.observe(el)\n\t\treturn () => ro.disconnect()\n\t}, [enableColumnResizing, userResized, getSubRows])\n\n\t// Only pin columns while the table actually overflows horizontally. When everything fits there's no\n\t// scroll to guard against, and a pinned cell (solid bg + border) would just read as an odd extra seam.\n\tconst [isOverflowing, setIsOverflowing] = React.useState(false)\n\tReact.useLayoutEffect(() => {\n\t\tconst tableEl = scrollRef.current?.querySelector('table')\n\t\tconst scroller = tableEl?.parentElement\n\t\tif (!tableEl || !scroller) return\n\t\tconst check = () => setIsOverflowing(tableEl.scrollWidth > scroller.clientWidth + 1)\n\t\tcheck()\n\t\tconst ro = new ResizeObserver(check)\n\t\tro.observe(scroller)\n\t\tro.observe(tableEl)\n\t\treturn () => ro.disconnect()\n\t}, [])\n\n\t// Which row the pointer is over, so a pinned cell can mirror the row's hover tint with inline styles.\n\tconst [hoveredRowId, setHoveredRowId] = React.useState<string | null>(null)\n\n\treturn (\n\t\t<div className=\"space-y-4\">\n\t\t\t{/* <DataTableToolbar table={table} /> */}\n\t\t\t<div ref={scrollRef} className=\"rounded-lg border border-border bg-background overflow-hidden\">\n\t\t\t\t{/* Size the table to the sum of column widths so columns resize independently and the table\n\t\t\t\t    scrolls horizontally instead of squashing neighbours. At rest the auto-fit effect makes\n\t\t\t\t    that total equal the container, so the columns fill the width. */}\n\t\t\t\t<Table\n\t\t\t\t\tclassName=\"table-fixed\"\n\t\t\t\t\tstyle={enableColumnResizing ? { width: table.getTotalSize() + (getSubRows ? EXPANDER_WIDTH : 0) } : undefined}\n\t\t\t\t>\n\t\t\t\t\t<TableHeader>\n\t\t\t\t\t\t{table.getHeaderGroups().map((headerGroup) => (\n\t\t\t\t\t\t\t<TableRow\n\t\t\t\t\t\t\t\tkey={headerGroup.id}\n\t\t\t\t\t\t\t\tclassName={cn(variant === 'muted' && 'bg-muted/50 text-muted-foreground hover:bg-muted/50')}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{getSubRows && <TableHead style={{ width: EXPANDER_WIDTH }} />}\n\t\t\t\t\t\t\t\t{headerGroup.headers.map((header, index) => {\n\t\t\t\t\t\t\t\t\t// No handle on the last column: it butts against the table border, so its divider would\n\t\t\t\t\t\t\t\t\t// double up with the border and read as an extra empty column.\n\t\t\t\t\t\t\t\t\tconst isLast = index === headerGroup.headers.length - 1\n\t\t\t\t\t\t\t\t\t// Actions columns are pinned everywhere by convention; other columns opt in via meta.pinRight.\n\t\t\t\t\t\t\t\t\t// Only while the table overflows horizontally (nothing to pin against otherwise).\n\t\t\t\t\t\t\t\t\tconst pinRight = isOverflowing && (header.column.columnDef.meta?.pinRight || header.column.id === 'actions')\n\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t<TableHead\n\t\t\t\t\t\t\t\t\t\t\tkey={header.id}\n\t\t\t\t\t\t\t\t\t\t\tid={`th-${header.id}`}\n\t\t\t\t\t\t\t\t\t\t\tcolSpan={header.colSpan}\n\t\t\t\t\t\t\t\t\t\t\t// A pinned (positioned) cell paints over the row's own border-b, so it carries its own.\n\t\t\t\t\t\t\t\t\t\t\t// Its opaque background goes through a class (fed by a variable), not an inline background,\n\t\t\t\t\t\t\t\t\t\t\t// so a theme's header colour still wins.\n\t\t\t\t\t\t\t\t\t\t\tclassName={cn(pinRight && 'border-b border-border bg-(--pinned-head-bg)')}\n\t\t\t\t\t\t\t\t\t\t\tstyle={\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\twidth: `${header.getSize()}px`,\n\t\t\t\t\t\t\t\t\t\t\t\t\t...(pinRight\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t...pinRightStyle,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tzIndex: 2,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t'--pinned-head-bg': variant === 'muted' ? MUTED_SURFACE : 'var(--background)',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: {}),\n\t\t\t\t\t\t\t\t\t\t\t\t} as React.CSSProperties\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{/* Relative wrapper (not the <th>, whose position:relative is unreliable across browsers)\n\t\t\t\t\t\t\t\t\t\t    so the absolute resize handle positions + stacks correctly. */}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"relative flex h-full items-center pr-1\">\n\t\t\t\t\t\t\t\t\t\t\t\t{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}\n\t\t\t\t\t\t\t\t\t\t\t\t{header.column.getCanResize() && !isLast && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<ResizeHandle\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tresizeHandler={header.getResizeHandler() as (e: unknown) => void}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tisResizing={header.column.getIsResizing()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</TableHead>\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</TableHeader>\n\t\t\t\t\t<TableBody>\n\t\t\t\t\t\t{table.getRowModel().rows?.length ? (\n\t\t\t\t\t\t\ttable.getRowModel().rows.map((row, rowIndex) => {\n\t\t\t\t\t\t\t\tconst isLastRow = rowIndex === table.getRowModel().rows.length - 1\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<TableRow\n\t\t\t\t\t\t\t\t\t\tkey={row.id}\n\t\t\t\t\t\t\t\t\t\tdata-state={row.getIsSelected() && 'selected'}\n\t\t\t\t\t\t\t\t\t\tclassName={cn(row.depth > 0 && 'bg-muted/50 text-muted-foreground')}\n\t\t\t\t\t\t\t\t\t\tonMouseEnter={() => setHoveredRowId(row.id)}\n\t\t\t\t\t\t\t\t\t\tonMouseLeave={() => setHoveredRowId((id) => (id === row.id ? null : id))}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{getSubRows && (\n\t\t\t\t\t\t\t\t\t\t\t<TableCell style={{ width: EXPANDER_WIDTH }}>\n\t\t\t\t\t\t\t\t\t\t\t\t{row.getCanExpand() && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\taria-expanded={row.getIsExpanded()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={row.getToggleExpandedHandler()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon-sm\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{row.getIsExpanded() ? <ChevronDownIcon /> : <ChevronRightIcon />}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t{row.getVisibleCells().map((cell) => {\n\t\t\t\t\t\t\t\t\t\t\tconst skipTruncate =\n\t\t\t\t\t\t\t\t\t\t\t\tcell.column.id === 'actions' ||\n\t\t\t\t\t\t\t\t\t\t\t\tcell.column.id === 'availability' ||\n\t\t\t\t\t\t\t\t\t\t\t\tcell.column.columnDef.meta?.truncate === false\n\t\t\t\t\t\t\t\t\t\t\tconst pinRight = isOverflowing && (cell.column.columnDef.meta?.pinRight || cell.column.id === 'actions')\n\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t<TableCell\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={cell.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\theaders={`th-${cell.column.id}`}\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Pinned cells paint over the row's border-b, so carry a matching one (except last row).\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName={pinRight && !isLastRow ? 'border-b border-border' : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={pinRight ? pinnedCellStyle(row.depth > 0, row.getIsSelected(), hoveredRowId === row.id) : undefined}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{skipTruncate ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tflexRender(cell.column.columnDef.cell, cell.getContext())\n\t\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"truncate\">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<TableRow>\n\t\t\t\t\t\t\t\t<TableCell colSpan={columns.length + (getSubRows ? 1 : 0)} className=\"h-24 text-center\">\n\t\t\t\t\t\t\t\t\tNo results.\n\t\t\t\t\t\t\t\t</TableCell>\n\t\t\t\t\t\t\t</TableRow>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</TableBody>\n\t\t\t\t</Table>\n\t\t\t</div>\n\t\t\t{table.getPageCount() > 1 && (\n\t\t\t\t<div className=\"flex flex-row justify-end\">\n\t\t\t\t\t<DataTablePagination onPaginationChange={handlePaginationChange} table={table} />\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/data-table.tsx"
    },
    {
      "path": "registry/rapid/ui/data-table/data.ts",
      "content": "import {\n\tArrowDownIcon,\n\tArrowRightIcon,\n\tArrowUpIcon,\n\tCircleCheckIcon,\n\tCircleDashedIcon,\n\tCircleIcon,\n\tCircleXIcon,\n\tHourglassIcon,\n\tTimerIcon,\n} from 'lucide-react'\n\n/** Case types, e.g. for a faceted filter on a case list. */\nexport const labels = [\n\t{\n\t\tvalue: 'building-permit',\n\t\tlabel: 'Building permit',\n\t},\n\t{\n\t\tvalue: 'environmental-permit',\n\t\tlabel: 'Environmental permit',\n\t},\n\t{\n\t\tvalue: 'records-request',\n\t\tlabel: 'Public records request',\n\t},\n]\n\n/** Case statuses, from registration to closing. */\nexport const statuses = [\n\t{\n\t\tvalue: 'received',\n\t\tlabel: 'Received',\n\t\ticon: CircleDashedIcon,\n\t},\n\t{\n\t\tvalue: 'in-review',\n\t\tlabel: 'In review',\n\t\ticon: TimerIcon,\n\t},\n\t{\n\t\tvalue: 'supplement',\n\t\tlabel: 'Waiting for supplement',\n\t\ticon: HourglassIcon,\n\t},\n\t{\n\t\tvalue: 'decided',\n\t\tlabel: 'Decided',\n\t\ticon: CircleCheckIcon,\n\t},\n\t{\n\t\tvalue: 'closed',\n\t\tlabel: 'Closed',\n\t\ticon: CircleIcon,\n\t},\n\t{\n\t\tvalue: 'withdrawn',\n\t\tlabel: 'Withdrawn',\n\t\ticon: CircleXIcon,\n\t},\n]\n\nexport const priorities = [\n\t{\n\t\tlabel: 'Low',\n\t\tvalue: 'low',\n\t\ticon: ArrowDownIcon,\n\t},\n\t{\n\t\tlabel: 'Normal',\n\t\tvalue: 'normal',\n\t\ticon: ArrowRightIcon,\n\t},\n\t{\n\t\tlabel: 'Urgent',\n\t\tvalue: 'urgent',\n\t\ticon: ArrowUpIcon,\n\t},\n]\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/data.ts"
    },
    {
      "path": "registry/rapid/ui/data-table/index.ts",
      "content": "export type {\n\tColumn,\n\tColumnDef,\n\tColumnFiltersState,\n\tSortingState,\n\tVisibilityState,\n} from '@tanstack/react-table'\nexport * from './creator-name-cell'\nexport * from './data'\nexport * from './data-table'\nexport * from './data-table-column-header'\nexport * from './data-table-faceted-filter'\nexport * from './data-table-pagination'\nexport * from './data-table-toolbar'\nexport * from './truncated-tooltip'\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/index.ts"
    },
    {
      "path": "registry/rapid/ui/data-table/truncated-tooltip.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'\n\n/** Single-line ellipsis: truncated when the content is wider than its clipped box. */\nexport function isEllipsisTruncated(el: HTMLElement | null): boolean {\n\treturn !!el && el.scrollWidth > el.clientWidth + 1\n}\n\n/**\n * Multi-line `-webkit-line-clamp`: truncated when the full text is taller than the clamped box.\n * Safari reports `scrollHeight === clientHeight` for clamped boxes, so lift the clamp to measure\n * the true height, then restore it.\n */\nexport function isLineClampTruncated(el: HTMLElement | null): boolean {\n\tif (!el) return false\n\tconst clampedHeight = el.clientHeight\n\tel.style.webkitLineClamp = '9999'\n\tconst fullHeight = el.scrollHeight\n\tel.style.webkitLineClamp = ''\n\treturn fullHeight > clampedHeight + 1\n}\n\ninterface TruncatedTooltipProps {\n\t/** Tooltip content — usually the full, untruncated text. */\n\tcontent: React.ReactNode\n\t/** `true` = multi-line line-clamp; `false` (default) = single-line ellipsis. */\n\tclamp?: boolean\n\t/** Delay in ms before the tooltip opens (passed to Base UI's `TooltipTrigger` `delay`). */\n\tdelayDuration?: number\n\tside?: 'top' | 'right' | 'bottom' | 'left'\n\talign?: 'start' | 'center' | 'end'\n\t/** Render the trigger; attach the passed `ref` to the element that is clipped/truncated. */\n\tchildren: (ref: React.RefCallback<HTMLElement>) => React.ReactElement\n}\n\n/**\n * Wraps a truncatable trigger and shows a tooltip ONLY when the text is actually truncated.\n * Measured on demand (hover/focus), when the element is guaranteed laid out and visible — this\n * avoids the layout-timing races of measuring inside an effect. The tooltip is non-interactive\n * (`pointer-events-none`) so it never blocks clicks on the rows beneath it.\n */\nexport function TruncatedTooltip({\n\tcontent,\n\tclamp = false,\n\tdelayDuration,\n\tside = 'bottom',\n\talign = 'start',\n\tchildren,\n}: Readonly<TruncatedTooltipProps>) {\n\tconst ref = React.useRef<HTMLElement | null>(null)\n\tconst [open, setOpen] = React.useState(false)\n\tconst check = () => (clamp ? isLineClampTruncated(ref.current) : isEllipsisTruncated(ref.current))\n\t// Stable ref callback so the trigger isn't detached/reattached on every render.\n\tconst setRef = React.useCallback<React.RefCallback<HTMLElement>>((el) => {\n\t\tref.current = el\n\t}, [])\n\n\treturn (\n\t\t<Tooltip open={open} onOpenChange={(next) => setOpen(next && check())}>\n\t\t\t<TooltipTrigger delay={delayDuration} render={children(setRef)} />\n\t\t\t<TooltipContent side={side} align={align} className=\"pointer-events-none\">\n\t\t\t\t{content}\n\t\t\t</TooltipContent>\n\t\t</Tooltip>\n\t)\n}\n",
      "type": "registry:ui",
      "target": "components/ui/data-table/truncated-tooltip.tsx"
    }
  ],
  "type": "registry:ui"
}