{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid-toolbar",
  "title": "Data Grid Toolbar",
  "author": "gridcn",
  "description": "Toolbar add-on for the gridcn data grid: quick-search with match navigation, a per-column filter menu (aligned rows, join column, drag-to-reorder via @dnd-kit/react), and a column visibility/pin menu.",
  "dependencies": [
    "@dnd-kit/dom",
    "@dnd-kit/react"
  ],
  "registryDependencies": [
    "DammersCode/gridcn/data-grid",
    "dropdown-menu",
    "popover",
    "input",
    "button",
    "badge",
    "select"
  ],
  "files": [
    {
      "path": "registry/default/blocks/data-grid-toolbar/data-grid-toolbar.ts",
      "content": "/** Public entry point — the only module consumers/docs should import from. */\r\n\r\nexport { DataGridToolbar, type DataGridToolbarProps } from \"./toolbar\";\r\nexport { DataGridSearch, type DataGridSearchProps } from \"./search\";\r\nexport { DataGridFilterMenu, type DataGridFilterMenuProps } from \"./filter-menu\";\r\nexport { DataGridColumnsMenu, type DataGridColumnsMenuProps } from \"./columns-menu\";\r\nexport { operatorsForColumnType, operatorLabel, operatorHasValue } from \"./operators-for-column-type\";\r\n",
      "type": "registry:component",
      "target": "components/data-grid-toolbar/data-grid-toolbar.ts"
    },
    {
      "path": "registry/default/blocks/data-grid-toolbar/toolbar.tsx",
      "content": "import type { ReactNode } from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n/** Props for {@link DataGridToolbar}. */\r\nexport type DataGridToolbarProps = {\r\n  className?: string;\r\n  children?: ReactNode;\r\n};\r\n\r\n/**\r\n * Flex frame for the grid's toolbar slots (search, filter menu, columns menu, custom buttons).\r\n * Consumers place it above `<DataGridRoot>`, inside the same `<DataGridProvider>`, so its children\r\n * share the grid's store.\r\n */\r\nexport function DataGridToolbar(props: DataGridToolbarProps): ReactNode {\r\n  const { className, children } = props;\r\n  return (\r\n    <div\r\n      data-grid-toolbar=\"\"\r\n      className={cn(\"flex items-center gap-2 overflow-x-auto border-b border-border bg-background px-2 py-1.5\", className)}\r\n    >\r\n      {children}\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-toolbar/toolbar.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid-toolbar/search.tsx",
      "content": "\"use client\";\r\n\r\nimport {\r\n  useCallback,\r\n  useEffect,\r\n  useRef,\r\n  useState,\r\n  type ChangeEvent,\r\n  type KeyboardEvent as ReactKeyboardEvent,\r\n  type ReactNode,\r\n} from \"react\";\r\nimport { ChevronDown, ChevronUp, Search } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n  useDataGridActions,\r\n  useDataGridLabels,\r\n  useDataGridScrollToCell,\r\n  useDataGridSearchCapped,\r\n  useDataGridSearchMatches,\r\n  useDataGridSearchText,\r\n  useDataGridVisibleColumns,\r\n  gridAttrSelector,\r\n} from \"@/registry/default/components/data-grid/data-grid\";\r\n\r\n/** Default debounce (ms) between typing and updating the store's `searchText`. */\r\nconst SEARCH_DEBOUNCE_MS = 200;\r\n\r\n/** Props for {@link DataGridSearch}. */\r\nexport type DataGridSearchProps = {\r\n  className?: string;\r\n  placeholder?: string;\r\n  /**\r\n   * Debounce (ms) between typing and the store's `searchText` update; default 200.\r\n   */\r\n  debounceMs?: number;\r\n  /**\r\n   * \"Feels native\" bridge: mod+F (Ctrl+F / Cmd+F) with focus inside the grid or its\r\n   * toolbar focuses/selects this input instead of opening the browser's find bar. Native\r\n   * find-in-page can't work over virtualized rows (only the window's rendered rows exist in the\r\n   * DOM), and our search already scrolls to matches — this makes mod+F reach for it directly.\r\n   * Default `true` — when this component is mounted and focus is inside the grid, mod+F\r\n   * unambiguously means \"search this grid\". The listener is grid-scoped (never window/document\r\n   * globally): focus anywhere else on the page is completely untouched, so a page's own search UI\r\n   * or the browser's native find behave exactly as if this prop didn't exist. Pass `false` to\r\n   * disable and always defer to the browser's native find.\r\n   */\r\n  captureFindShortcut?: boolean;\r\n};\r\n\r\n/** True when `target` sits inside the grid's own DOM (the scroll/root container or the toolbar), never outside it. */\r\nfunction isInsideGridScope(target: EventTarget | null): boolean {\r\n  if (!(target instanceof Element)) return false;\r\n  return target.closest(`[role=\"grid\"], ${gridAttrSelector(\"toolbar\")}`) !== null;\r\n}\r\n\r\n/**\r\n * Quick-search input: debounced write to the store's `searchText`, a match-count badge\r\n * (\"3/17\"), and prev/next controls that step through matches in row-major order, moving the\r\n * active cell to the match and scrolling it into view. Enter/Shift+Enter mirror the buttons;\r\n * Escape clears the field and the store's search text.\r\n */\r\nexport function DataGridSearch(props: DataGridSearchProps): ReactNode {\r\n  const { className, placeholder, captureFindShortcut = true, debounceMs = SEARCH_DEBOUNCE_MS } = props;\r\n  const labels = useDataGridLabels();\r\n  const actions = useDataGridActions();\r\n  const searchText = useDataGridSearchText();\r\n  const matches = useDataGridSearchMatches();\r\n  const capped = useDataGridSearchCapped();\r\n  const visibleColumns = useDataGridVisibleColumns();\r\n  const scrollToCell = useDataGridScrollToCell();\r\n\r\n  // local input value decoupled from the store's debounced searchText, so typing never stalls.\r\n  const [inputValue, setInputValue] = useState(searchText);\r\n  const [matchIndex, setMatchIndex] = useState(0);\r\n  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n  const inputRef = useRef<HTMLInputElement | null>(null);\r\n\r\n  // an external clear (e.g. filter reset) should resync the visible input text.\r\n  useEffect(() => {\r\n    setInputValue(searchText);\r\n  }, [searchText]);\r\n\r\n  // the match list changes shape (new search, data change) — restart navigation at the first hit.\r\n  useEffect(() => {\r\n    setMatchIndex(0);\r\n  }, [matches]);\r\n\r\n  useEffect(() => () => {\r\n    if (debounceRef.current) clearTimeout(debounceRef.current);\r\n  }, []);\r\n\r\n  // Grid-scoped mod+F capture: no DOM ancestor spans both the toolbar and DataGridRoot (siblings\r\n  // under DataGridProvider, which renders no wrapper element), so this listens on `document` and\r\n  // gates on isInsideGridScope — same closest('[role=\"grid\"]') resolution context-menu.tsx uses\r\n  // for reaching outside DataGridRoot's own subtree. Native find is left untouched outside the grid.\r\n  useEffect(() => {\r\n    if (!captureFindShortcut) return;\r\n\r\n    function onKeyDown(event: KeyboardEvent) {\r\n      const isFind = (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === \"f\";\r\n      if (!isFind || !isInsideGridScope(event.target)) return;\r\n      event.preventDefault();\r\n      const input = inputRef.current;\r\n      if (!input) return;\r\n      if (document.activeElement === input) {\r\n        input.select();\r\n      } else {\r\n        input.focus();\r\n      }\r\n    }\r\n\r\n    document.addEventListener(\"keydown\", onKeyDown);\r\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\r\n  }, [captureFindShortcut]);\r\n\r\n  const commitSearch = useCallback(\r\n    (value: string) => {\r\n      if (debounceRef.current) clearTimeout(debounceRef.current);\r\n      debounceRef.current = setTimeout(() => actions.setSearch(value), debounceMs);\r\n    },\r\n    [actions, debounceMs],\r\n  );\r\n\r\n  const goToMatch = useCallback(\r\n    (index: number) => {\r\n      if (matches.length === 0) return;\r\n      const wrapped = ((index % matches.length) + matches.length) % matches.length;\r\n      setMatchIndex(wrapped);\r\n      const match = matches[wrapped]!; // wrapped is modulo'd into [0, matches.length)\r\n      const col = visibleColumns.findIndex((c) => c.id === match.columnId);\r\n      if (col === -1) return;\r\n      const coord = { col, row: match.row };\r\n      actions.setActiveCell(coord);\r\n      scrollToCell(coord);\r\n    },\r\n    [actions, scrollToCell, matches, visibleColumns],\r\n  );\r\n\r\n  const onChange = useCallback(\r\n    (event: ChangeEvent<HTMLInputElement>) => {\r\n      setInputValue(event.target.value);\r\n      commitSearch(event.target.value);\r\n    },\r\n    [commitSearch],\r\n  );\r\n\r\n  const clear = useCallback(() => {\r\n    if (debounceRef.current) clearTimeout(debounceRef.current);\r\n    setInputValue(\"\");\r\n    actions.setSearch(\"\");\r\n  }, [actions]);\r\n\r\n  const onKeyDown = useCallback(\r\n    (event: ReactKeyboardEvent<HTMLInputElement>) => {\r\n      if (event.key === \"Escape\") {\r\n        event.preventDefault();\r\n        clear();\r\n      } else if (event.key === \"Enter\") {\r\n        event.preventDefault();\r\n        goToMatch(matchIndex + (event.shiftKey ? -1 : 1));\r\n      }\r\n    },\r\n    [clear, goToMatch, matchIndex],\r\n  );\r\n\r\n  const hasMatches = matches.length > 0;\r\n\r\n  return (\r\n    <div data-grid-search=\"\" className={cn(\"flex h-8 items-center gap-1\", className)}>\r\n      <div className=\"relative\">\r\n        <Search className=\"pointer-events-none absolute inset-s-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\r\n        <Input\r\n          ref={inputRef}\r\n          value={inputValue}\r\n          onChange={onChange}\r\n          onKeyDown={onKeyDown}\r\n          placeholder={placeholder ?? labels.toolbar.searchPlaceholder}\r\n          aria-label={labels.toolbar.searchAriaLabel}\r\n          className=\"h-8 w-56 ps-7\"\r\n        />\r\n      </div>\r\n      {searchText.trim() !== \"\" && (\r\n        <span\r\n          data-grid-search-count=\"\"\r\n          role=\"status\"\r\n          aria-live=\"polite\"\r\n          className=\"min-w-fit shrink-0 text-xs tabular-nums text-muted-foreground\"\r\n        >\r\n          {hasMatches\r\n            ? capped\r\n              ? labels.toolbar.searchMatchesCapped(matchIndex + 1)\r\n              : labels.toolbar.searchMatches(matchIndex + 1, matches.length)\r\n            : labels.toolbar.searchNoMatches}\r\n        </span>\r\n      )}\r\n      <Button\r\n        type=\"button\"\r\n        variant=\"ghost\"\r\n        size=\"icon-sm\"\r\n        aria-label={labels.toolbar.searchPreviousMatch}\r\n        disabled={!hasMatches}\r\n        onClick={() => goToMatch(matchIndex - 1)}\r\n      >\r\n        <ChevronUp />\r\n      </Button>\r\n      <Button\r\n        type=\"button\"\r\n        variant=\"ghost\"\r\n        size=\"icon-sm\"\r\n        aria-label={labels.toolbar.searchNextMatch}\r\n        disabled={!hasMatches}\r\n        onClick={() => goToMatch(matchIndex + 1)}\r\n      >\r\n        <ChevronDown />\r\n      </Button>\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-toolbar/search.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid-toolbar/filter-menu.tsx",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from \"react\";\r\nimport { AutoScroller } from \"@dnd-kit/dom\";\r\nimport { DragDropProvider } from \"@dnd-kit/react\";\r\nimport { useSortable } from \"@dnd-kit/react/sortable\";\r\nimport { Filter, GripVertical, Trash2 } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\r\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\r\nimport {\r\n  isDev,\r\n  useDataGridActions,\r\n  useDataGridAllColumns,\r\n  useDataGridFilterState,\r\n  useDataGridJoinOperator,\r\n  useDataGridLabels,\r\n  useDataGridVisibleColumns,\r\n  type AnyColumnDef,\r\n  type DataGridLabels,\r\n  type FilterJoinOperator,\r\n  type FilterOperator,\r\n  type FilterSpec,\r\n} from \"@/registry/default/components/data-grid/data-grid\";\r\nimport { operatorHasValue, operatorLabel, operatorsForColumnType } from \"./operators-for-column-type\";\r\nimport { DataGridFilterValueInput } from \"./filter-value-input\";\r\n\r\n/** Props for {@link DataGridFilterMenu}. */\r\nexport type DataGridFilterMenuProps = {\r\n  className?: string;\r\n  /**\r\n   * Source the column options from every column (including hidden ones) instead of only the visible\r\n   * columns; default false. Without it, a filter set on a hidden column (programmatically or via a\r\n   * restored layout) keeps applying but renders with no column context and logs a dev-time warning.\r\n   */\r\n  allColumns?: boolean;\r\n  /**\r\n   * Replaces the operator list the menu offers per column. Resolution order: this prop (a returned\r\n   * `undefined` falls through), then the column's own `filterOperators`, then the built-in\r\n   * `operatorsForColumnType` for its `type`.\r\n   */\r\n  operatorsForColumn?: (column: AnyColumnDef) => readonly FilterOperator[] | undefined;\r\n};\r\n\r\n/** dnd-kit drag payload for a filter row: compiler-checked in place of `Record<string, any>`. */\r\ntype FilterRowDragData = { index: number; filterId: string };\r\n\r\n/** Column label for a select item/trigger: `headerText`, else the string `header`, else the id. */\r\nfunction columnLabel(column: AnyColumnDef): string {\r\n  return column.headerText ?? (typeof column.header === \"string\" ? column.header : column.id);\r\n}\r\n\r\n/** Moves the item at `fromIndex` to `toIndex`, returning a new array (no mutation of `list`). */\r\nfunction moveItem<T>(list: T[], fromIndex: number, toIndex: number): T[] {\r\n  const next = list.slice();\r\n  const [moved] = next.splice(fromIndex, 1);\r\n  if (moved === undefined) return list;\r\n  next.splice(toIndex, 0, moved);\r\n  return next;\r\n}\r\n\r\n/**\r\n * Popover listing active filters as one aligned grid row per filter — anatomy ported from tablecn's\r\n * `DataTableFilterList` (a join cell: \"Where\" on row 1, an And/Or select on row 2, static join text\r\n * on rows 3+; field select; operator select; typed value input; trash icon-button; grip drag\r\n * handle; footer \"Add filter\"/\"Reset filters\" buttons) but built from this repo's own Base UI\r\n * components/conventions rather than transliterated from tablecn's Radix markup. Reordering uses\r\n * `@dnd-kit/react`'s sortable primitives (handle-initiated pointer drag with a lifted-row overlay\r\n * feel via `isDragSource`), plus a plain ArrowUp/ArrowDown-on-grip fallback that always wins over\r\n * the library's own keyboard sensor (see {@link FilterRow}'s `onKeyDown`). Rows are keyed by\r\n * `FilterSpec.filterId` (stable identity independent of array position) since multiple filter rows\r\n * on the same column are allowed and meaningful; row order is array order in `FilterSpec[]`,\r\n * persisted through `setFilters` on every reorder.\r\n */\r\nexport function DataGridFilterMenu(props: DataGridFilterMenuProps): ReactNode {\r\n  const { className, allColumns = false, operatorsForColumn } = props;\r\n  const actions = useDataGridActions();\r\n  const filters = useDataGridFilterState();\r\n  const joinOperator = useDataGridJoinOperator();\r\n  const visibleColumns = useDataGridVisibleColumns();\r\n  const gridColumns = useDataGridAllColumns();\r\n  const columns = allColumns ? gridColumns : visibleColumns;\r\n  const labels = useDataGridLabels();\r\n  const filterableColumns = columns.filter((c) => c.filterable !== false);\r\n\r\n  // per-column operator list: this prop > the column's filterOperators > the built-in type default\r\n  const operatorsFor = useCallback(\r\n    (column: AnyColumnDef | undefined): readonly FilterOperator[] =>\r\n      (column && operatorsForColumn ? operatorsForColumn(column) : undefined) ?? column?.filterOperators ?? operatorsForColumnType(column?.type),\r\n    [operatorsForColumn],\r\n  );\r\n\r\n  // A filter on a column outside the menu's option list (a hidden column by default, or one no\r\n  // longer in the grid) still applies to the view but renders with no column context and no picker\r\n  // entry to re-target it — warn in dev and point at the fix.\r\n  useEffect(() => {\r\n    if (!isDev()) return;\r\n    const listed = new Set(columns.map((c) => c.id));\r\n    const orphans = filters.filter((f) => !listed.has(f.columnId));\r\n    if (orphans.length === 0) return;\r\n    const ids = Array.from(new Set(orphans.map((f) => f.columnId)));\r\n    console.warn(\r\n      `[data-grid-toolbar] DataGridFilterMenu: ${orphans.length} filter row(s) target column(s) missing from the menu's column list (${ids.join(\", \")}); pass allColumns to manage hidden-column filters here`,\r\n    );\r\n  }, [filters, columns]);\r\n\r\n  const [announcement, setAnnouncement] = useState(\"\");\r\n  const gripRefs = useRef(new Map<string, HTMLButtonElement | null>());\r\n  const addButtonRef = useRef<HTMLButtonElement>(null);\r\n  const pendingFocusFilterIdRef = useRef<string | null>(null);\r\n\r\n  // ponytail: lock root scroll during a drag - at drag end the browser re-focuses the grip and natively re-centers it in the viewport, jumping the page\r\n  const rootScrollLockRef = useRef<(() => void) | null>(null);\r\n  const lockRootScroll = useCallback(() => {\r\n    if (rootScrollLockRef.current) return;\r\n    const y = window.scrollY;\r\n    const onScroll = () => {\r\n      if (window.scrollY !== y) window.scrollTo(0, y);\r\n    };\r\n    document.addEventListener(\"scroll\", onScroll, true);\r\n    rootScrollLockRef.current = () => {\r\n      document.removeEventListener(\"scroll\", onScroll, true);\r\n      rootScrollLockRef.current = null;\r\n    };\r\n  }, []);\r\n  const unlockRootScroll = useCallback(() => rootScrollLockRef.current?.(), []);\r\n\r\n  useEffect(() => {\r\n    const pendingId = pendingFocusFilterIdRef.current;\r\n    if (!pendingId) return;\r\n    pendingFocusFilterIdRef.current = null;\r\n    gripRefs.current.get(pendingId)?.focus({ preventScroll: true });\r\n  }, [filters]);\r\n\r\n  useEffect(() => unlockRootScroll, [unlockRootScroll]);\r\n\r\n  const updateFilter = useCallback(\r\n    (filterId: string | undefined, next: Partial<FilterSpec>) => {\r\n      actions.setFilters(filters.map((f) => (f.filterId === filterId ? { ...f, ...next } : f)));\r\n    },\r\n    [actions, filters],\r\n  );\r\n\r\n  const removeFilter = useCallback(\r\n    (filterId: string | undefined, index: number) => {\r\n      actions.setFilters(filters.filter((f) => f.filterId !== filterId));\r\n      // focus follows deletion: the next row's grip, or the Add button once the list empties.\r\n      const nextFocusId = filters[index + 1]?.filterId ?? filters[index - 1]?.filterId;\r\n      if (nextFocusId) pendingFocusFilterIdRef.current = nextFocusId;\r\n      else requestAnimationFrame(() => addButtonRef.current?.focus({ preventScroll: true }));\r\n    },\r\n    [actions, filters],\r\n  );\r\n\r\n  const addFilter = useCallback(() => {\r\n    const first = filterableColumns[0];\r\n    if (!first) return;\r\n    const operator = operatorsFor(first)[0]!; // always non-empty (TEXT_OPERATORS is the floor)\r\n    actions.setFilters([...filters, { columnId: first.id, operator, value: \"\" }]);\r\n  }, [actions, filterableColumns, filters, operatorsFor]);\r\n\r\n  const resetFilters = useCallback(() => {\r\n    actions.setFilters([]);\r\n    actions.setJoinOperator(\"and\");\r\n  }, [actions]);\r\n\r\n  const announceMove = useCallback(\r\n    (filterId: string, toIndex: number, total: number) => {\r\n      const filter = filters.find((f) => f.filterId === filterId);\r\n      const column = columns.find((c) => c.id === filter?.columnId);\r\n      const label = column ? columnLabel(column) : (filter?.columnId ?? \"\");\r\n      setAnnouncement(labels.toolbar.filterReorderAnnouncement(label, toIndex + 1, total));\r\n    },\r\n    [filters, columns, labels],\r\n  );\r\n\r\n  const reorderFilters = useCallback(\r\n    (fromIndex: number, toIndex: number, filterId: string) => {\r\n      if (fromIndex === toIndex) return;\r\n      const next = moveItem(filters, fromIndex, toIndex);\r\n      actions.setFilters(next);\r\n      pendingFocusFilterIdRef.current = filterId;\r\n      announceMove(filterId, toIndex, next.length);\r\n    },\r\n    [actions, filters, announceMove],\r\n  );\r\n\r\n  return (\r\n    <Popover>\r\n      <PopoverTrigger\r\n        render={\r\n          <Button type=\"button\" variant=\"outline\" size=\"sm\" className={cn(\"relative\", className)} aria-label={labels.toolbar.filterAriaLabel}>\r\n            <Filter />\r\n            {labels.toolbar.filter}\r\n            {filters.length > 0 && (\r\n              <Badge data-grid-filter-count=\"\" variant=\"secondary\" className=\"ms-1\">\r\n                {filters.length}\r\n              </Badge>\r\n            )}\r\n          </Button>\r\n        }\r\n      />\r\n      <PopoverContent align=\"start\" className=\"flex w-full min-w-105 flex-col gap-3.5 p-4\">\r\n        <div data-grid-filter-menu=\"\" className=\"flex flex-col gap-3.5\">\r\n          <h4 className=\"font-medium leading-none text-muted-foreground\">{labels.toolbar.filter}</h4>\r\n          {filters.length === 0 && <p className=\"text-sm text-muted-foreground\">{labels.toolbar.noFiltersApplied}</p>}\r\n          <div aria-live=\"polite\" role=\"status\" className=\"sr-only\">\r\n            {announcement}\r\n          </div>\r\n          {filters.length > 0 && (\r\n            <DragDropProvider<FilterRowDragData>\r\n              // ponytail: drop AutoScroller — the popover portals to body, so the page is a scrollable\r\n              // ancestor and dragging near a viewport edge scrolls the page; re-add it if the list\r\n              // (max-h-75) overflows often enough that mid-drag auto-scrolling is needed\r\n              plugins={(defaults) => defaults.filter((p) => p !== AutoScroller)}\r\n              onDragStart={lockRootScroll}\r\n              onDragEnd={(event) => {\r\n                unlockRootScroll();\r\n                if (event.canceled) return;\r\n                const sourceData = event.operation.source?.data;\r\n                const targetData = event.operation.target?.data;\r\n                const fromIndex = sourceData?.index;\r\n                const toIndex = targetData?.index;\r\n                const filterId = sourceData?.filterId;\r\n                if (fromIndex === undefined || toIndex === undefined || !filterId) return;\r\n                reorderFilters(fromIndex, toIndex, filterId);\r\n              }}\r\n            >\r\n              <div role=\"list\" className=\"flex max-h-75 flex-col gap-2 overflow-y-auto p-1\">\r\n                {filters.map((filter, index) => {\r\n                  const column = columns.find((c) => c.id === filter.columnId);\r\n                  return (\r\n                    <FilterRow\r\n                      key={filter.filterId}\r\n                      filter={filter}\r\n                      index={index}\r\n                      total={filters.length}\r\n                      column={column}\r\n                      joinOperator={joinOperator}\r\n                      columns={columns}\r\n                      filterableColumns={filterableColumns}\r\n                      operatorsFor={operatorsFor}\r\n                      labels={labels}\r\n                      onJoinOperatorChange={actions.setJoinOperator}\r\n                      onUpdate={updateFilter}\r\n                      onRemove={removeFilter}\r\n                      onArrowReorder={(toIndex) => reorderFilters(index, toIndex, filter.filterId!)}\r\n                      gripRef={(el) => {\r\n                        gripRefs.current.set(filter.filterId!, el);\r\n                      }}\r\n                    />\r\n                  );\r\n                })}\r\n              </div>\r\n            </DragDropProvider>\r\n          )}\r\n          <div className=\"flex w-full items-center gap-2\">\r\n            <Button type=\"button\" ref={addButtonRef} onClick={addFilter} disabled={filterableColumns.length === 0}>\r\n              {labels.toolbar.addFilter}\r\n            </Button>\r\n            {filters.length > 0 && (\r\n              <Button type=\"button\" variant=\"outline\" onClick={resetFilters}>\r\n                {labels.toolbar.clearFilters}\r\n              </Button>\r\n            )}\r\n          </div>\r\n        </div>\r\n      </PopoverContent>\r\n    </Popover>\r\n  );\r\n}\r\n\r\ntype FilterRowProps = {\r\n  filter: FilterSpec;\r\n  index: number;\r\n  total: number;\r\n  column: AnyColumnDef | undefined;\r\n  joinOperator: FilterJoinOperator;\r\n  columns: readonly AnyColumnDef[];\r\n  filterableColumns: readonly AnyColumnDef[];\r\n  operatorsFor: (column: AnyColumnDef | undefined) => readonly FilterOperator[];\r\n  labels: DataGridLabels;\r\n  onJoinOperatorChange: (value: FilterJoinOperator) => void;\r\n  onUpdate: (filterId: string | undefined, next: Partial<FilterSpec>) => void;\r\n  onRemove: (filterId: string | undefined, index: number) => void;\r\n  onArrowReorder: (toIndex: number) => void;\r\n  gripRef: (el: HTMLButtonElement | null) => void;\r\n};\r\n\r\n/** One filter row: `useSortable` wires pointer-drag reorder via `@dnd-kit/react`; the grip's `onKeyDown` implements the ArrowUp/ArrowDown fallback, which always takes precedence over dnd-kit's own keyboard sensor (checked first, `stopPropagation`'d). */\r\nfunction FilterRow(props: FilterRowProps): ReactNode {\r\n  const { filter, index, total, column, joinOperator, columns, filterableColumns, operatorsFor, labels, onJoinOperatorChange, onUpdate, onRemove, onArrowReorder, gripRef } = props;\r\n  const operators = operatorsFor(column);\r\n  // dnd-kit's default plugins include OptimisticSortingPlugin, which physically moves DOM nodes\r\n  // mid-drag — that fights React's own re-render once `onDragEnd` commits the new `filters` order\r\n  // via setFilters, leaving rows with the wrong join-cell content after a drop. Passing an empty\r\n  // plugins array drops it (and the default keyboard plugin); this row's own `onGripKeyDown` below\r\n  // is the only keyboard reorder path, so nothing is lost. The DOM then only ever reorders once,\r\n  // driven by React itself from the committed store order.\r\n  const { ref, handleRef, isDragSource } = useSortable<FilterRowDragData>({\r\n    id: filter.filterId!,\r\n    index,\r\n    data: { index, filterId: filter.filterId! }, // filterId always assigned by the store; see FilterSpec\r\n    plugins: [],\r\n  });\r\n\r\n  const onGripKeyDown = useCallback(\r\n    (event: ReactKeyboardEvent<HTMLButtonElement>) => {\r\n      if (event.key !== \"ArrowUp\" && event.key !== \"ArrowDown\") return;\r\n      event.preventDefault();\r\n      event.stopPropagation(); // wins over dnd-kit's keyboard sensor regardless of library support\r\n      const target = index + (event.key === \"ArrowUp\" ? -1 : 1);\r\n      if (target < 0 || target >= total) return;\r\n      onArrowReorder(target);\r\n    },\r\n    [index, total, onArrowReorder],\r\n  );\r\n\r\n  return (\r\n    <div\r\n      ref={ref}\r\n      data-grid-filter-row=\"\"\r\n      role=\"listitem\"\r\n      className={cn(\r\n        \"grid grid-cols-[minmax(4.5rem,auto)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.5fr)_auto_auto] items-center gap-1.5 transition-transform\",\r\n        isDragSource && \"z-10 scale-[1.01] opacity-90 shadow-lg\",\r\n      )}\r\n    >\r\n      <div className=\"min-w-18 text-center\">\r\n        {index === 0 ? (\r\n          <span className=\"text-sm text-muted-foreground\">{labels.toolbar.filterWhere}</span>\r\n        ) : index === 1 ? (\r\n          <Select\r\n            value={joinOperator}\r\n            onValueChange={(value) => {\r\n              if (value === null) return;\r\n              onJoinOperatorChange(value as FilterJoinOperator);\r\n            }}\r\n          >\r\n            <SelectTrigger className=\"h-8 w-full\" aria-label={labels.toolbar.joinOperatorAriaLabel}>\r\n              {/* explicit label lookup: SelectValue only resolves a mounted SelectItem's label, which\r\n                  the popup content isn't until first opened — this control's own trigger text must be\r\n                  correct on first render (no open required) since it appears the moment 2+ filters exist. */}\r\n              <SelectValue>{(value: FilterJoinOperator) => (value === \"or\" ? labels.toolbar.joinOperatorOr : labels.toolbar.joinOperatorAnd)}</SelectValue>\r\n            </SelectTrigger>\r\n            <SelectContent>\r\n              <SelectItem value=\"and\">{labels.toolbar.joinOperatorAnd}</SelectItem>\r\n              <SelectItem value=\"or\">{labels.toolbar.joinOperatorOr}</SelectItem>\r\n            </SelectContent>\r\n          </Select>\r\n        ) : (\r\n          <span className=\"text-sm text-muted-foreground\">{joinOperator === \"or\" ? labels.toolbar.joinOperatorOr : labels.toolbar.joinOperatorAnd}</span>\r\n        )}\r\n      </div>\r\n      <Select\r\n        value={filter.columnId}\r\n        onValueChange={(value) => {\r\n          if (value === null) return;\r\n          const nextColumn = columns.find((c) => c.id === value);\r\n          const nextOperators = operatorsFor(nextColumn);\r\n          const nextOperator = nextOperators.includes(filter.operator) ? filter.operator : nextOperators[0];\r\n          onUpdate(filter.filterId, { columnId: value, operator: nextOperator, value: \"\" });\r\n        }}\r\n      >\r\n        <SelectTrigger className=\"h-8 w-full\" aria-label={labels.toolbar.filterColumnAriaLabel}>\r\n          {/* explicit label lookup, same rationale as the join-operator select above. */}\r\n          <SelectValue className=\"truncate\">{(value: string) => { const c = filterableColumns.find((col) => col.id === value); return c ? columnLabel(c) : value; }}</SelectValue>\r\n        </SelectTrigger>\r\n        <SelectContent>\r\n          {filterableColumns.map((c) => (\r\n            <SelectItem key={c.id} value={c.id}>\r\n              {columnLabel(c)}\r\n            </SelectItem>\r\n          ))}\r\n        </SelectContent>\r\n      </Select>\r\n      <Select\r\n        value={filter.operator}\r\n        onValueChange={(value) => {\r\n          if (value === null) return;\r\n          const nextOperator = value as FilterOperator;\r\n          onUpdate(filter.filterId, {\r\n            operator: nextOperator,\r\n            // isBetween's value shape differs from every other operator's; reset rather than carry a mismatched shape.\r\n            value: nextOperator === \"isBetween\" ? [\"\", \"\"] : \"\",\r\n          });\r\n        }}\r\n      >\r\n        <SelectTrigger className=\"h-8 w-full\" aria-label={labels.toolbar.filterOperatorAriaLabel}>\r\n          {/* explicit label lookup, same rationale as the join-operator select above. */}\r\n          <SelectValue className=\"truncate\">{(value: FilterOperator) => operatorLabel(value, labels.filterOperators)}</SelectValue>\r\n        </SelectTrigger>\r\n        <SelectContent>\r\n          {operators.map((op) => (\r\n            <SelectItem key={op} value={op}>\r\n              {operatorLabel(op, labels.filterOperators)}\r\n            </SelectItem>\r\n          ))}\r\n        </SelectContent>\r\n      </Select>\r\n      <div className=\"min-w-0\">\r\n        {operatorHasValue(filter.operator) ? (\r\n          <DataGridFilterValueInput column={column} operator={filter.operator} value={filter.value} onValueChange={(value) => onUpdate(filter.filterId, { value })} />\r\n        ) : (\r\n          <div className=\"h-8 w-full rounded border bg-transparent\" />\r\n        )}\r\n      </div>\r\n      <Button type=\"button\" variant=\"outline\" size=\"icon\" className=\"size-8 shrink-0\" aria-label={labels.toolbar.removeFilterAriaLabel} onClick={() => onRemove(filter.filterId, index)}>\r\n        <Trash2 />\r\n      </Button>\r\n      <Button\r\n        type=\"button\"\r\n        variant=\"outline\"\r\n        size=\"icon\"\r\n        ref={(el) => {\r\n          handleRef(el);\r\n          gripRef(el);\r\n        }}\r\n        className={cn(\"size-8 shrink-0 cursor-grab touch-none\", isDragSource && \"cursor-grabbing\")}\r\n        aria-label={labels.toolbar.reorderFilterAriaLabel}\r\n        // prevent native mousedown focus: Chromium re-centers the focused grip in the viewport mid-drag and the page jumps\r\n        onMouseDown={(e) => e.preventDefault()}\r\n        onKeyDown={onGripKeyDown}\r\n      >\r\n        <GripVertical />\r\n      </Button>\r\n    </div>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-toolbar/filter-menu.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid-toolbar/filter-value-input.tsx",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useEffect, useRef, useState, type ChangeEvent, type ReactNode } from \"react\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\";\r\nimport {\r\n  useDataGridLabels,\r\n  type AnyColumnDef,\r\n  type FilterOperator,\r\n  type GridCellTypes,\r\n} from \"@/registry/default/components/data-grid/data-grid\";\r\nimport {\r\n  DropdownMenu,\r\n  DropdownMenuCheckboxItem,\r\n  DropdownMenuContent,\r\n  DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { operatorIsAnyOf, operatorIsBetween } from \"./operators-for-column-type\";\r\n\r\n/** Debounce (ms) between typing and committing a filter value, matching {@link DataGridSearch}. */\r\nconst FILTER_VALUE_DEBOUNCE_MS = 200;\r\n\r\n/** Props for {@link DataGridFilterValueInput}. */\r\nexport type DataGridFilterValueInputProps = {\r\n  /** The filtered column, for choosing a typed input (number/date/checkbox/select) over the plain-text default. */\r\n  column: AnyColumnDef | undefined;\r\n  operator: FilterOperator;\r\n  value: string | [string, string] | string[] | undefined;\r\n  onValueChange: (value: string | [string, string] | string[]) => void;\r\n};\r\n\r\n/** Debounces a single text/number/date input's keystrokes into one commit, matching {@link DataGridSearch}. */\r\nfunction useDebouncedInput(value: string, onValueChange: (value: string) => void) {\r\n  const [inputValue, setInputValue] = useState(value);\r\n  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n\r\n  // an external change (e.g. clear all, column swap) should resync the visible text.\r\n  useEffect(() => {\r\n    setInputValue(value);\r\n  }, [value]);\r\n\r\n  useEffect(() => () => {\r\n    if (debounceRef.current) clearTimeout(debounceRef.current);\r\n  }, []);\r\n\r\n  const onChange = useCallback(\r\n    (event: ChangeEvent<HTMLInputElement>) => {\r\n      const next = event.target.value;\r\n      setInputValue(next);\r\n      if (debounceRef.current) clearTimeout(debounceRef.current);\r\n      debounceRef.current = setTimeout(() => onValueChange(next), FILTER_VALUE_DEBOUNCE_MS);\r\n    },\r\n    [onValueChange],\r\n  );\r\n\r\n  return { inputValue, onChange };\r\n}\r\n\r\n/** A single debounced text/number/date value input, shared by the plain and `isBetween` (two-input) layouts. */\r\nfunction SingleValueInput(props: {\r\n  type: \"text\" | \"number\" | \"date\";\r\n  value: string;\r\n  onValueChange: (value: string) => void;\r\n  ariaLabel: string;\r\n  placeholder?: string;\r\n}) {\r\n  const { type, value, onValueChange, ariaLabel, placeholder } = props;\r\n  const { inputValue, onChange } = useDebouncedInput(value, onValueChange);\r\n  return (\r\n    <Input\r\n      type={type}\r\n      value={inputValue}\r\n      onChange={onChange}\r\n      placeholder={placeholder}\r\n      aria-label={ariaLabel}\r\n      className=\"h-8 w-full\"\r\n    />\r\n  );\r\n}\r\n\r\n/**\r\n * The `isBetween` two-input range layout. Each side debounces independently (same as a lone value\r\n * input), but both commits read the OTHER side's value from a ref at commit time rather than closing\r\n * over the render-time value — two edits within the same debounce window (e.g. filling \"from\" then\r\n * \"to\" quickly) would otherwise race: whichever debounce fires last overwrites the other's edit with\r\n * the stale value it captured 200ms earlier.\r\n */\r\nfunction BetweenValueInput(props: {\r\n  type: \"text\" | \"number\" | \"date\";\r\n  from: string;\r\n  to: string;\r\n  onValueChange: (value: [string, string]) => void;\r\n  fromAriaLabel: string;\r\n  toAriaLabel: string;\r\n}) {\r\n  const { type, from, to, onValueChange, fromAriaLabel, toAriaLabel } = props;\r\n  const fromRef = useRef(from);\r\n  const toRef = useRef(to);\r\n  fromRef.current = from;\r\n  toRef.current = to;\r\n\r\n  const commitFrom = useCallback((next: string) => onValueChange([next, toRef.current]), [onValueChange]);\r\n  const commitTo = useCallback((next: string) => onValueChange([fromRef.current, next]), [onValueChange]);\r\n\r\n  const fromInput = useDebouncedInput(from, commitFrom);\r\n  const toInput = useDebouncedInput(to, commitTo);\r\n\r\n  return (\r\n    <div className=\"flex items-center gap-1.5\">\r\n      <Input type={type} value={fromInput.inputValue} onChange={fromInput.onChange} aria-label={fromAriaLabel} className=\"h-8 w-full\" />\r\n      <Input type={type} value={toInput.inputValue} onChange={toInput.onChange} aria-label={toAriaLabel} className=\"h-8 w-full\" />\r\n    </div>\r\n  );\r\n}\r\n\r\n/**\r\n * The `isAnyOf` multi-choice input: a dropdown of the select column's own `choices`, each a\r\n * checkbox. Commits immediately (no debounce — a click is already a deliberate, discrete choice,\r\n * unlike a keystroke).\r\n */\r\nfunction AnyOfValueInput(props: {\r\n  choices: readonly { value: string; label: string }[];\r\n  selected: readonly string[];\r\n  onValueChange: (value: string[]) => void;\r\n  ariaLabel: string;\r\n  summary: string;\r\n}) {\r\n  const { choices, selected, onValueChange, ariaLabel, summary } = props;\r\n  const toggle = (choice: string, checked: boolean) => {\r\n    // rebuild from `choices` so the committed order is the column's own, not click order\r\n    const next = new Set(selected);\r\n    if (checked) next.add(choice);\r\n    else next.delete(choice);\r\n    onValueChange(choices.map((c) => c.value).filter((v) => next.has(v)));\r\n  };\r\n\r\n  return (\r\n    <DropdownMenu>\r\n      <DropdownMenuTrigger\r\n        render={\r\n          <Button type=\"button\" variant=\"outline\" className=\"h-8 w-full justify-start font-normal\" aria-label={ariaLabel}>\r\n            <span className=\"truncate\">{summary}</span>\r\n          </Button>\r\n        }\r\n      />\r\n      <DropdownMenuContent align=\"start\">\r\n        {choices.map((choice) => (\r\n          <DropdownMenuCheckboxItem\r\n            key={choice.value}\r\n            checked={selected.includes(choice.value)}\r\n            onCheckedChange={(checked) => toggle(choice.value, checked)}\r\n          >\r\n            {choice.label}\r\n          </DropdownMenuCheckboxItem>\r\n        ))}\r\n      </DropdownMenuContent>\r\n    </DropdownMenu>\r\n  );\r\n}\r\n\r\n/**\r\n * Filter-row value input: renders a typed input per the filtered column's cell type — number columns\r\n * get `<Input type=\"number\">`, date columns a date input, checkbox columns a true/false select, select\r\n * columns a select of their own choices, everything else plain text — and debounces text/number/date\r\n * keystrokes so typing never triggers a `setFilters` view-index rebuild per character.\r\n * `isBetween` renders two of the column's typed input side by side instead of one.\r\n */\r\nexport function DataGridFilterValueInput(props: DataGridFilterValueInputProps): ReactNode {\r\n  const { column, operator, value, onValueChange } = props;\r\n  const labels = useDataGridLabels();\r\n  const columnType = column?.type;\r\n\r\n  if (operatorIsAnyOf(operator)) {\r\n    const choices = (column?.options as GridCellTypes[\"select\"][\"options\"] | undefined)?.choices ?? [];\r\n    const selected = Array.isArray(value) ? value : [];\r\n    return (\r\n      <AnyOfValueInput\r\n        choices={choices}\r\n        selected={selected}\r\n        onValueChange={onValueChange}\r\n        ariaLabel={labels.toolbar.filterValueAriaLabel}\r\n        summary={labels.toolbar.filterValueAnyOfSummary(selected.length)}\r\n      />\r\n    );\r\n  }\r\n\r\n  if (operatorIsBetween(operator)) {\r\n    const [from = \"\", to = \"\"] = Array.isArray(value) ? value : [\"\", \"\"];\r\n    const inputType = columnType === \"number\" ? \"number\" : columnType === \"date\" ? \"date\" : \"text\";\r\n    return (\r\n      <BetweenValueInput\r\n        type={inputType}\r\n        from={from}\r\n        to={to}\r\n        onValueChange={onValueChange}\r\n        fromAriaLabel={labels.toolbar.filterValueFromAriaLabel}\r\n        toAriaLabel={labels.toolbar.filterValueToAriaLabel}\r\n      />\r\n    );\r\n  }\r\n\r\n  const textValue = typeof value === \"string\" ? value : \"\";\r\n\r\n  if (columnType === \"checkbox\") {\r\n    return (\r\n      <Select value={textValue} onValueChange={(next) => onValueChange(next ?? \"\")}>\r\n        <SelectTrigger className=\"h-8 w-full\" aria-label={labels.toolbar.filterValueAriaLabel}>\r\n          <SelectValue placeholder={labels.toolbar.filterValuePlaceholder} />\r\n        </SelectTrigger>\r\n        <SelectContent>\r\n          <SelectItem value=\"true\">{labels.toolbar.filterValueTrue}</SelectItem>\r\n          <SelectItem value=\"false\">{labels.toolbar.filterValueFalse}</SelectItem>\r\n        </SelectContent>\r\n      </Select>\r\n    );\r\n  }\r\n\r\n  if (columnType === \"select\") {\r\n    const choices = (column?.options as GridCellTypes[\"select\"][\"options\"] | undefined)?.choices ?? [];\r\n    return (\r\n      <Select value={textValue} onValueChange={(next) => onValueChange(next ?? \"\")}>\r\n        <SelectTrigger className=\"h-8 w-full\" aria-label={labels.toolbar.filterValueAriaLabel}>\r\n          <SelectValue placeholder={labels.toolbar.filterValuePlaceholder} />\r\n        </SelectTrigger>\r\n        <SelectContent>\r\n          {choices.map((choice) => (\r\n            <SelectItem key={choice.value} value={choice.value}>\r\n              {choice.label}\r\n            </SelectItem>\r\n          ))}\r\n        </SelectContent>\r\n      </Select>\r\n    );\r\n  }\r\n\r\n  const inputType = columnType === \"number\" ? \"number\" : columnType === \"date\" ? \"date\" : \"text\";\r\n  return (\r\n    <SingleValueInput\r\n      type={inputType}\r\n      value={textValue}\r\n      onValueChange={onValueChange}\r\n      ariaLabel={labels.toolbar.filterValueAriaLabel}\r\n      placeholder={labels.toolbar.filterValuePlaceholder}\r\n    />\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-toolbar/filter-value-input.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid-toolbar/columns-menu.tsx",
      "content": "\"use client\";\r\n\r\nimport type { ReactNode } from \"react\";\r\nimport { Settings2 } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n  DropdownMenu,\r\n  DropdownMenuCheckboxItem,\r\n  DropdownMenuContent,\r\n  DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport {\r\n  useDataGridActions,\r\n  useDataGridAllColumns,\r\n  useDataGridIsColumnHidden,\r\n  useDataGridLabels,\r\n  type AnyColumnDef,\r\n} from \"@/registry/default/components/data-grid/data-grid\";\r\n\r\n/** Props for {@link DataGridColumnsMenu}. */\r\nexport type DataGridColumnsMenuProps = {\r\n  className?: string;\r\n};\r\n\r\n/** Column label for the menu row: `headerText`, else the string `header`, else the id. */\r\nfunction columnLabel(column: AnyColumnDef): string {\r\n  return column.headerText ?? (typeof column.header === \"string\" ? column.header : column.id);\r\n}\r\n\r\n/** One column's visibility row; a separate component so each row subscribes to its own hidden state. */\r\nfunction ColumnRow(props: { column: AnyColumnDef }): ReactNode {\r\n  const { column } = props;\r\n  const actions = useDataGridActions();\r\n  const hidden = useDataGridIsColumnHidden(column.id);\r\n\r\n  return (\r\n    <DropdownMenuCheckboxItem\r\n      checked={!hidden}\r\n      onCheckedChange={(checked) => actions.setColumnHidden(column.id, checked !== true)}\r\n    >\r\n      {columnLabel(column)}\r\n    </DropdownMenuCheckboxItem>\r\n  );\r\n}\r\n\r\n/**\r\n * Show/hide-only column visibility dropdown — pin controls live in the header dropdown /\r\n * context menu instead. Each row is a\r\n * `DropdownMenuCheckboxItem` with the standard trailing check-mark indicator, not a square Checkbox.\r\n */\r\nexport function DataGridColumnsMenu(props: DataGridColumnsMenuProps): ReactNode {\r\n  const { className } = props;\r\n  const columns = useDataGridAllColumns();\r\n  const labels = useDataGridLabels();\r\n\r\n  return (\r\n    <DropdownMenu>\r\n      <DropdownMenuTrigger\r\n        render={\r\n          <Button type=\"button\" variant=\"outline\" size=\"sm\" className={cn(className)} aria-label={labels.toolbar.columnsAriaLabel}>\r\n            <Settings2 />\r\n            {labels.toolbar.columns}\r\n          </Button>\r\n        }\r\n      />\r\n      <DropdownMenuContent data-grid-columns-menu=\"\" align=\"start\" className=\"w-56\">\r\n        {columns.map((column) => (\r\n          <ColumnRow key={column.id} column={column} />\r\n        ))}\r\n      </DropdownMenuContent>\r\n    </DropdownMenu>\r\n  );\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-toolbar/columns-menu.tsx"
    },
    {
      "path": "registry/default/blocks/data-grid-toolbar/operators-for-column-type.ts",
      "content": "import type { DataGridFilterOperatorLabels, FilterOperator } from \"@/registry/default/components/data-grid/data-grid\";\r\n\r\n/** Text/general operators offered for every column type. */\r\nconst TEXT_OPERATORS: FilterOperator[] = [\r\n  \"contains\",\r\n  \"notContains\",\r\n  \"equals\",\r\n  \"notEquals\",\r\n  \"startsWith\",\r\n  \"endsWith\",\r\n  \"empty\",\r\n  \"notEmpty\",\r\n];\r\n\r\n/** Comparison operators added only for orderable types (gt/lt only for number/date). */\r\nconst COMPARISON_OPERATORS: FilterOperator[] = [\"gt\", \"gte\", \"lt\", \"lte\", \"isBetween\"];\r\n\r\n/**\r\n * Sensible {@link FilterOperator} choices for a column's cell type: comparisons only apply to\r\n * number/date, and `isAnyOf` only to select columns, whose `options.choices` are what the\r\n * multi-value input offers.\r\n */\r\nexport function operatorsForColumnType(columnType: string | undefined): FilterOperator[] {\r\n  if (columnType === \"number\" || columnType === \"date\") {\r\n    return [...TEXT_OPERATORS, ...COMPARISON_OPERATORS];\r\n  }\r\n  if (columnType === \"select\") return [...TEXT_OPERATORS, \"isAnyOf\"];\r\n  return TEXT_OPERATORS;\r\n}\r\n\r\n/** Human-readable label for a {@link FilterOperator}, for the operator select — reads from {@link useDataGridLabels}'s `filterOperators` group. */\r\nexport function operatorLabel(operator: FilterOperator, labels: DataGridFilterOperatorLabels): string {\r\n  return labels[operator];\r\n}\r\n\r\n/** Whether `operator` takes a value input at all (`empty`/`notEmpty` don't); `isBetween` renders two. */\r\nexport function operatorHasValue(operator: FilterOperator): boolean {\r\n  return operator !== \"empty\" && operator !== \"notEmpty\";\r\n}\r\n\r\n/** Whether `operator` needs the two-input `isBetween` range layout instead of a single value input. */\r\nexport function operatorIsBetween(operator: FilterOperator): boolean {\r\n  return operator === \"isBetween\";\r\n}\r\n\r\n/** Whether `operator` takes a multi-value choice list (`isAnyOf`) rather than a single text input. */\r\nexport function operatorIsAnyOf(operator: FilterOperator): boolean {\r\n  return operator === \"isAnyOf\";\r\n}\r\n",
      "type": "registry:component",
      "target": "components/data-grid-toolbar/operators-for-column-type.ts"
    }
  ],
  "categories": [
    "data"
  ],
  "type": "registry:block"
}
