{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid-filter-menu",
  "title": "Data Grid Filter Menu",
  "description": "A filter menu component for the data grid with drag-and-drop reordering and advanced filtering",
  "dependencies": [
    "@tanstack/react-table",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "calendar",
    "command",
    "input",
    "popover",
    "select"
  ],
  "files": [
    {
      "path": "src/components/data-grid/data-grid-filter-menu.tsx",
      "content": "\"use client\";\n\nimport type { Column, ColumnFilter, Table } from \"@tanstack/react-table\";\nimport {\n  CalendarIcon,\n  Check,\n  ChevronsUpDown,\n  GripVertical,\n  ListFilter,\n  Trash2,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport { useDirection } from \"@/components/ui/direction\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  Sortable,\n  SortableContent,\n  SortableItem,\n  SortableItemHandle,\n  SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport {\n  getDefaultOperator,\n  getOperatorsForVariant,\n} from \"@/lib/data-grid-filters\";\nimport { formatDate } from \"@/lib/format\";\nimport { cn } from \"@/lib/utils\";\nimport type { FilterOperator, FilterValue } from \"@/types/data-grid\";\n\nconst FILTER_SHORTCUT_KEY = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = new Set([\"backspace\", \"delete\"]);\nconst FILTER_DEBOUNCE_MS = 300;\nconst OPERATORS_WITHOUT_VALUE = new Set([\n  \"isEmpty\",\n  \"isNotEmpty\",\n  \"isTrue\",\n  \"isFalse\",\n]);\n\ninterface DataGridFilterMenuProps<TData>\n  extends React.ComponentProps<typeof PopoverContent> {\n  table: Table<TData>;\n  disabled?: boolean;\n}\n\nexport function DataGridFilterMenu<TData>({\n  table,\n  disabled,\n  className,\n  ...props\n}: DataGridFilterMenuProps<TData>) {\n  const dir = useDirection();\n  const id = React.useId();\n  const labelId = React.useId();\n  const descriptionId = React.useId();\n  const [open, setOpen] = React.useState(false);\n  const addButtonRef = React.useRef<HTMLButtonElement>(null);\n\n  const columnFilters = table.getState().columnFilters;\n\n  const { columnLabels, columns, columnVariants } = React.useMemo(() => {\n    const labels = new Map<string, string>();\n    const variants = new Map<string, string>();\n    const filteringIds = new Set(columnFilters.map((f) => f.id));\n    const availableColumns: { id: string; label: string }[] = [];\n\n    for (const column of table.getAllColumns()) {\n      if (!column.getCanFilter()) continue;\n\n      const label = column.columnDef.meta?.label ?? column.id;\n      const variant = column.columnDef.meta?.cell?.variant ?? \"short-text\";\n\n      labels.set(column.id, label);\n      variants.set(column.id, variant);\n\n      if (!filteringIds.has(column.id)) {\n        availableColumns.push({ id: column.id, label });\n      }\n    }\n\n    return {\n      columnLabels: labels,\n      columns: availableColumns,\n      columnVariants: variants,\n    };\n  }, [columnFilters, table]);\n\n  const onFilterAdd = React.useCallback(() => {\n    const firstColumn = columns[0];\n    if (!firstColumn) return;\n\n    const variant = columnVariants.get(firstColumn.id) ?? \"short-text\";\n    const defaultOperator = getDefaultOperator(variant);\n\n    table.setColumnFilters((prevFilters) => [\n      ...prevFilters,\n      {\n        id: firstColumn.id,\n        value: {\n          operator: defaultOperator,\n          value: \"\",\n        },\n      },\n    ]);\n  }, [columns, columnVariants, table]);\n\n  const onFilterUpdate = React.useCallback(\n    (filterId: string, updates: Partial<ColumnFilter>) => {\n      table.setColumnFilters((prevFilters) => {\n        if (!prevFilters) return prevFilters;\n        return prevFilters.map((filter) =>\n          filter.id === filterId ? { ...filter, ...updates } : filter,\n        );\n      });\n    },\n    [table],\n  );\n\n  const onFilterRemove = React.useCallback(\n    (filterId: string) => {\n      table.setColumnFilters((prevFilters) =>\n        prevFilters.filter((item) => item.id !== filterId),\n      );\n    },\n    [table],\n  );\n\n  const onFiltersReset = React.useCallback(() => {\n    table.setColumnFilters(table.initialState.columnFilters ?? []);\n  }, [table]);\n\n  React.useEffect(() => {\n    function onKeyDown(event: KeyboardEvent) {\n      if (\n        event.target instanceof HTMLInputElement ||\n        event.target instanceof HTMLTextAreaElement ||\n        (event.target instanceof HTMLElement &&\n          event.target.contentEditable === \"true\")\n      ) {\n        return;\n      }\n\n      if (\n        event.key.toLowerCase() === FILTER_SHORTCUT_KEY &&\n        (event.ctrlKey || event.metaKey) &&\n        event.shiftKey\n      ) {\n        event.preventDefault();\n        setOpen((prev) => !prev);\n      }\n    }\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => window.removeEventListener(\"keydown\", onKeyDown);\n  }, []);\n\n  const onTriggerKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      if (\n        REMOVE_FILTER_SHORTCUTS.has(event.key.toLowerCase()) &&\n        columnFilters.length > 0\n      ) {\n        event.preventDefault();\n        onFiltersReset();\n      }\n    },\n    [columnFilters.length, onFiltersReset],\n  );\n\n  return (\n    <Sortable\n      value={columnFilters}\n      onValueChange={table.setColumnFilters}\n      getItemValue={(item) => item.id}\n    >\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button\n            dir={dir}\n            variant=\"outline\"\n            className=\"font-normal\"\n            onKeyDown={onTriggerKeyDown}\n            disabled={disabled}\n          >\n            <ListFilter className=\"text-muted-foreground\" />\n            Filter\n            {columnFilters.length > 0 && (\n              <Badge\n                variant=\"secondary\"\n                className=\"h-[18.24px] rounded-[3.2px] px-[5.12px] font-mono font-normal text-[10.4px]\"\n              >\n                {columnFilters.length}\n              </Badge>\n            )}\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent\n          aria-labelledby={labelId}\n          aria-describedby={descriptionId}\n          dir={dir}\n          className={cn(\n            \"flex w-full max-w-(--radix-popover-content-available-width) flex-col gap-3.5 p-4 sm:min-w-[480px]\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"flex flex-col gap-1\">\n            <h4 id={labelId} className=\"font-medium leading-none\">\n              {columnFilters.length > 0 ? \"Filter by\" : \"No filters applied\"}\n            </h4>\n            <p\n              id={descriptionId}\n              className={cn(\n                \"text-muted-foreground text-sm\",\n                columnFilters.length > 0 && \"sr-only\",\n              )}\n            >\n              {columnFilters.length > 0\n                ? \"Modify filters to narrow down your data.\"\n                : \"Add filters to narrow down your data.\"}\n            </p>\n          </div>\n          {columnFilters.length > 0 && (\n            <SortableContent asChild>\n              <div\n                role=\"list\"\n                className=\"flex max-h-[400px] flex-col gap-2 overflow-y-auto p-1\"\n              >\n                {columnFilters.map((filter, index) => (\n                  <DataGridFilterItem\n                    key={filter.id}\n                    filter={filter}\n                    index={index}\n                    filterItemId={`${id}-filter-${filter.id}`}\n                    dir={dir}\n                    columns={columns}\n                    columnLabels={columnLabels}\n                    columnVariants={columnVariants}\n                    table={table}\n                    onFilterUpdate={onFilterUpdate}\n                    onFilterRemove={onFilterRemove}\n                  />\n                ))}\n              </div>\n            </SortableContent>\n          )}\n          <div className=\"flex w-full items-center gap-2\">\n            <Button\n              className=\"rounded\"\n              ref={addButtonRef}\n              onClick={onFilterAdd}\n              disabled={columns.length === 0}\n            >\n              Add filter\n            </Button>\n            {columnFilters.length > 0 && (\n              <Button\n                variant=\"outline\"\n                className=\"rounded\"\n                onClick={onFiltersReset}\n              >\n                Reset filters\n              </Button>\n            )}\n          </div>\n        </PopoverContent>\n      </Popover>\n      <SortableOverlay>\n        <div dir={dir} className=\"flex items-center gap-2\">\n          <div className=\"h-8 min-w-[72px] rounded-sm bg-primary/10\" />\n          <div className=\"h-8 w-32 rounded-sm bg-primary/10\" />\n          <div className=\"h-8 w-32 rounded-sm bg-primary/10\" />\n          <div className=\"h-8 w-36 rounded-sm bg-primary/10\" />\n          <div className=\"size-8 shrink-0 rounded-sm bg-primary/10\" />\n          <div className=\"size-8 shrink-0 rounded-sm bg-primary/10\" />\n        </div>\n      </SortableOverlay>\n    </Sortable>\n  );\n}\n\ninterface DataGridFilterItemProps<TData> {\n  filter: ColumnFilter;\n  index: number;\n  filterItemId: string;\n  dir: \"ltr\" | \"rtl\";\n  columns: { id: string; label: string }[];\n  columnLabels: Map<string, string>;\n  columnVariants: Map<string, string>;\n  table: Table<TData>;\n  onFilterUpdate: (filterId: string, updates: Partial<ColumnFilter>) => void;\n  onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataGridFilterItem<TData>({\n  filter,\n  index,\n  filterItemId,\n  dir,\n  columns,\n  columnLabels,\n  columnVariants,\n  table,\n  onFilterUpdate,\n  onFilterRemove,\n}: DataGridFilterItemProps<TData>) {\n  const fieldListboxId = `${filterItemId}-field-listbox`;\n  const fieldTriggerId = `${filterItemId}-field-trigger`;\n  const operatorListboxId = `${filterItemId}-operator-listbox`;\n  const inputId = `${filterItemId}-input`;\n\n  const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n  const [showOperatorSelector, setShowOperatorSelector] = React.useState(false);\n\n  const variant = columnVariants.get(filter.id) ?? \"short-text\";\n  const filterValue = filter.value as FilterValue | undefined;\n  const operator = filterValue?.operator ?? getDefaultOperator(variant);\n\n  const operators = getOperatorsForVariant(variant);\n  const needsValue = !OPERATORS_WITHOUT_VALUE.has(operator);\n\n  const column = table.getColumn(filter.id);\n\n  const onItemKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (\n        event.target instanceof HTMLInputElement ||\n        event.target instanceof HTMLTextAreaElement\n      ) {\n        return;\n      }\n\n      if (showFieldSelector || showOperatorSelector) {\n        return;\n      }\n\n      if (REMOVE_FILTER_SHORTCUTS.has(event.key.toLowerCase())) {\n        event.preventDefault();\n        onFilterRemove(filter.id);\n      }\n    },\n    [filter.id, showFieldSelector, showOperatorSelector, onFilterRemove],\n  );\n\n  const onOperatorChange = React.useCallback(\n    (newOperator: FilterOperator) => {\n      onFilterUpdate(filter.id, {\n        value: {\n          operator: newOperator,\n          value: filterValue?.value,\n          endValue: filterValue?.endValue,\n        },\n      });\n    },\n    [filter.id, filterValue?.value, filterValue?.endValue, onFilterUpdate],\n  );\n\n  const onValueChange = React.useCallback(\n    (newValue: string | number | string[] | undefined) => {\n      onFilterUpdate(filter.id, {\n        value: {\n          operator,\n          value: newValue,\n          endValue: filterValue?.endValue,\n        },\n      });\n    },\n    [filter.id, operator, filterValue?.endValue, onFilterUpdate],\n  );\n\n  const onEndValueChange = React.useCallback(\n    (newValue: string | number | string[] | undefined) => {\n      onFilterUpdate(filter.id, {\n        value: {\n          operator,\n          value: filterValue?.value,\n          endValue: newValue as string | number | undefined,\n        },\n      });\n    },\n    [filter.id, operator, filterValue?.value, onFilterUpdate],\n  );\n\n  return (\n    <SortableItem value={filter.id} asChild>\n      <div\n        role=\"listitem\"\n        id={filterItemId}\n        tabIndex={-1}\n        className=\"flex items-center gap-2\"\n        onKeyDown={onItemKeyDown}\n      >\n        <div className=\"min-w-[72px] text-center\">\n          {index === 0 ? (\n            <span className=\"text-muted-foreground text-sm\">Where</span>\n          ) : (\n            <span className=\"text-muted-foreground text-sm\">And</span>\n          )}\n        </div>\n        <Popover open={showFieldSelector} onOpenChange={setShowFieldSelector}>\n          <PopoverTrigger asChild>\n            <Button\n              id={fieldTriggerId}\n              aria-controls={fieldListboxId}\n              dir={dir}\n              variant=\"outline\"\n              className=\"w-32 justify-between rounded font-normal\"\n            >\n              <span className=\"truncate\">{columnLabels.get(filter.id)}</span>\n              <ChevronsUpDown className=\"opacity-50\" />\n            </Button>\n          </PopoverTrigger>\n          <PopoverContent\n            id={fieldListboxId}\n            dir={dir}\n            align=\"start\"\n            className=\"w-40 p-0\"\n          >\n            <Command>\n              <CommandInput placeholder=\"Search fields...\" />\n              <CommandList>\n                <CommandEmpty>No fields found.</CommandEmpty>\n                <CommandGroup>\n                  {columns.map((column) => (\n                    <CommandItem\n                      key={column.id}\n                      value={column.id}\n                      onSelect={(value) => {\n                        const newVariant =\n                          columnVariants.get(value) ?? \"short-text\";\n                        const newOperator = getDefaultOperator(newVariant);\n\n                        table.setColumnFilters((prevFilters) =>\n                          prevFilters.map((f) =>\n                            f.id === filter.id\n                              ? {\n                                  id: value,\n                                  value: {\n                                    operator: newOperator,\n                                    value: \"\",\n                                  },\n                                }\n                              : f,\n                          ),\n                        );\n                        setShowFieldSelector(false);\n                      }}\n                    >\n                      <span className=\"truncate\">{column.label}</span>\n                      <Check\n                        className={cn(\n                          \"ms-auto\",\n                          column.id === filter.id ? \"opacity-100\" : \"opacity-0\",\n                        )}\n                      />\n                    </CommandItem>\n                  ))}\n                </CommandGroup>\n              </CommandList>\n            </Command>\n          </PopoverContent>\n        </Popover>\n        <Select\n          open={showOperatorSelector}\n          onOpenChange={setShowOperatorSelector}\n          value={operator}\n          onValueChange={onOperatorChange}\n        >\n          <SelectTrigger\n            aria-controls={operatorListboxId}\n            className=\"w-32 rounded lowercase\"\n          >\n            <div className=\"truncate\">\n              <SelectValue />\n            </div>\n          </SelectTrigger>\n          <SelectContent id={operatorListboxId}>\n            <SelectGroup>\n              {operators.map((op) => (\n                <SelectItem\n                  key={op.value}\n                  value={op.value}\n                  className=\"lowercase\"\n                >\n                  {op.label}\n                </SelectItem>\n              ))}\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n        <div className=\"min-w-36 max-w-60 flex-1\">\n          {needsValue && column ? (\n            <DataGridFilterInput\n              key={filter.id}\n              variant={variant}\n              operator={operator}\n              column={column}\n              inputId={inputId}\n              dir={dir}\n              value={filterValue?.value}\n              endValue={filterValue?.endValue}\n              onValueChange={onValueChange}\n              onEndValueChange={onEndValueChange}\n            />\n          ) : (\n            <div\n              id={inputId}\n              role=\"status\"\n              aria-label={`${columnLabels.get(filter.id)} filter is empty`}\n              aria-live=\"polite\"\n              className=\"h-8 w-full rounded border bg-transparent dark:bg-input/30\"\n            />\n          )}\n        </div>\n        <Button\n          aria-controls={filterItemId}\n          variant=\"outline\"\n          size=\"icon\"\n          className=\"size-8 rounded\"\n          onClick={() => onFilterRemove(filter.id)}\n        >\n          <Trash2 />\n        </Button>\n        <SortableItemHandle asChild>\n          <Button variant=\"outline\" size=\"icon\" className=\"size-8 rounded\">\n            <GripVertical />\n          </Button>\n        </SortableItemHandle>\n      </div>\n    </SortableItem>\n  );\n}\n\ninterface DataGridFilterInputProps<TData> {\n  variant: string;\n  operator: FilterOperator;\n  dir: \"ltr\" | \"rtl\";\n  placeholder?: string;\n  value: string | number | string[] | undefined;\n  endValue?: string | number;\n  column: Column<TData>;\n  inputId: string;\n  onValueChange: (value: string | number | string[] | undefined) => void;\n  onEndValueChange?: (value: string | number | string[] | undefined) => void;\n}\n\nfunction DataGridFilterInput<TData>({\n  variant,\n  operator,\n  dir,\n  placeholder = \"Value\",\n  value,\n  endValue,\n  column,\n  inputId,\n  onValueChange,\n  onEndValueChange,\n}: DataGridFilterInputProps<TData>) {\n  const [showValueSelector, setShowValueSelector] = React.useState(false);\n  const [localValue, setLocalValue] = React.useState(value);\n  const [localEndValue, setLocalEndValue] = React.useState(endValue);\n\n  const debouncedOnChange = useDebouncedCallback(\n    (newValue: string | number | string[] | undefined) => {\n      onValueChange(newValue);\n    },\n    FILTER_DEBOUNCE_MS,\n  );\n\n  const debouncedOnEndValueChange = useDebouncedCallback(\n    (newValue: string | number | string[] | undefined) => {\n      onEndValueChange?.(newValue);\n    },\n    FILTER_DEBOUNCE_MS,\n  );\n\n  const cellVariant = column.columnDef.meta?.cell;\n\n  const selectOptions = React.useMemo(() => {\n    return cellVariant?.variant === \"select\" ||\n      cellVariant?.variant === \"multi-select\"\n      ? cellVariant.options\n      : [];\n  }, [cellVariant]);\n\n  const isBetween = operator === \"isBetween\";\n\n  if (variant === \"number\") {\n    if (isBetween) {\n      return (\n        <div className=\"flex gap-2\">\n          <Input\n            id={inputId}\n            type=\"number\"\n            inputMode=\"numeric\"\n            placeholder=\"Start\"\n            value={(localValue as number | undefined) ?? \"\"}\n            onChange={(event) => {\n              const val = event.target.value;\n              const newValue = val === \"\" ? undefined : Number(val);\n              setLocalValue(newValue);\n              debouncedOnChange(newValue);\n            }}\n            className=\"h-8 w-full flex-1 rounded\"\n          />\n          <Input\n            id={`${inputId}-end`}\n            type=\"number\"\n            inputMode=\"numeric\"\n            placeholder=\"End\"\n            value={(localEndValue as number | undefined) ?? \"\"}\n            onChange={(event) => {\n              const val = event.target.value;\n              const newValue = val === \"\" ? undefined : Number(val);\n              setLocalEndValue(newValue);\n              debouncedOnEndValueChange(newValue);\n            }}\n            className=\"h-8 w-full flex-1 rounded\"\n          />\n        </div>\n      );\n    }\n\n    return (\n      <Input\n        id={inputId}\n        type=\"number\"\n        inputMode=\"numeric\"\n        placeholder={placeholder}\n        value={(localValue as number | undefined) ?? \"\"}\n        onChange={(event) => {\n          const val = event.target.value;\n          const newValue = val === \"\" ? undefined : Number(val);\n          setLocalValue(newValue);\n          debouncedOnChange(newValue);\n        }}\n        className=\"h-8 w-full rounded\"\n      />\n    );\n  }\n\n  if (variant === \"date\") {\n    const inputListboxId = `${inputId}-listbox`;\n\n    if (isBetween) {\n      const startDate =\n        localValue && typeof localValue === \"string\"\n          ? new Date(localValue)\n          : undefined;\n      const endDate =\n        localEndValue && typeof localEndValue === \"string\"\n          ? new Date(localEndValue)\n          : undefined;\n\n      const isSameDate =\n        startDate &&\n        endDate &&\n        startDate.toDateString() === endDate.toDateString();\n\n      const displayValue =\n        startDate && endDate && !isSameDate\n          ? `${formatDate(startDate, { month: \"short\" })} - ${formatDate(endDate, { month: \"short\" })}`\n          : startDate\n            ? formatDate(startDate, { month: \"short\" })\n            : \"Pick a range\";\n\n      return (\n        <Popover open={showValueSelector} onOpenChange={setShowValueSelector}>\n          <PopoverTrigger asChild>\n            <Button\n              id={inputId}\n              aria-controls={inputListboxId}\n              dir={dir}\n              variant=\"outline\"\n              className={cn(\n                \"h-8 w-full justify-start rounded font-normal\",\n                !startDate && \"text-muted-foreground\",\n              )}\n            >\n              <CalendarIcon />\n              <span className=\"truncate\">{displayValue}</span>\n            </Button>\n          </PopoverTrigger>\n          <PopoverContent\n            id={inputListboxId}\n            dir={dir}\n            align=\"start\"\n            className=\"w-auto p-0\"\n          >\n            <Calendar\n              autoFocus\n              captionLayout=\"dropdown\"\n              mode=\"range\"\n              selected={\n                startDate && endDate\n                  ? { from: startDate, to: endDate }\n                  : startDate\n                    ? { from: startDate, to: startDate }\n                    : undefined\n              }\n              onSelect={(range) => {\n                const fromValue = range?.from\n                  ? range.from.toISOString()\n                  : undefined;\n                const toValue = range?.to ? range.to.toISOString() : undefined;\n                setLocalValue(fromValue);\n                setLocalEndValue(toValue);\n                onValueChange(fromValue);\n                onEndValueChange?.(toValue);\n              }}\n            />\n          </PopoverContent>\n        </Popover>\n      );\n    }\n\n    const dateValue =\n      localValue && typeof localValue === \"string\"\n        ? new Date(localValue)\n        : undefined;\n\n    return (\n      <Popover open={showValueSelector} onOpenChange={setShowValueSelector}>\n        <PopoverTrigger asChild>\n          <Button\n            id={inputId}\n            aria-controls={inputListboxId}\n            dir={dir}\n            variant=\"outline\"\n            className={cn(\n              \"h-8 w-full justify-start rounded font-normal\",\n              !dateValue && \"text-muted-foreground\",\n            )}\n          >\n            <CalendarIcon />\n            <span className=\"truncate\">\n              {dateValue\n                ? formatDate(dateValue, { month: \"short\" })\n                : \"Pick a date\"}\n            </span>\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent\n          id={inputListboxId}\n          dir={dir}\n          align=\"start\"\n          className=\"w-auto p-0\"\n        >\n          <Calendar\n            autoFocus\n            captionLayout=\"dropdown\"\n            mode=\"single\"\n            selected={dateValue}\n            onSelect={(date) => {\n              const newValue = date ? date.toISOString() : undefined;\n              setLocalValue(newValue);\n              onValueChange(newValue);\n              setShowValueSelector(false);\n            }}\n          />\n        </PopoverContent>\n      </Popover>\n    );\n  }\n\n  const isSelectVariant = variant === \"select\" || variant === \"multi-select\";\n  const isMultiValueOperator =\n    operator === \"isAnyOf\" || operator === \"isNoneOf\";\n\n  if (isSelectVariant && selectOptions.length > 0) {\n    const inputListboxId = `${inputId}-listbox`;\n\n    if (isMultiValueOperator) {\n      const selectedValues = Array.isArray(value) ? value : [];\n      const selectedOptions = selectOptions.filter((option) =>\n        selectedValues.includes(option.value),\n      );\n\n      const selectedOptionsWithIcons = selectedOptions.filter(\n        (selectedOption) => selectedOption.icon,\n      );\n\n      return (\n        <Popover open={showValueSelector} onOpenChange={setShowValueSelector}>\n          <PopoverTrigger asChild>\n            <Button\n              id={inputId}\n              aria-controls={inputListboxId}\n              dir={dir}\n              variant=\"outline\"\n              className=\"h-8 w-full justify-start rounded font-normal\"\n            >\n              {selectedOptions.length === 0 ? (\n                <span className=\"text-muted-foreground\">{placeholder}</span>\n              ) : (\n                <>\n                  {selectedOptionsWithIcons.length > 0 && (\n                    <div className=\"flex items-center -space-x-2 rtl:space-x-reverse\">\n                      {selectedOptionsWithIcons.map(\n                        (selectedOption) =>\n                          selectedOption.icon && (\n                            <div\n                              key={selectedOption.value}\n                              className=\"rounded-full border bg-background p-0.5\"\n                            >\n                              <selectedOption.icon className=\"size-3.5\" />\n                            </div>\n                          ),\n                      )}\n                    </div>\n                  )}\n                  <span className=\"truncate\">\n                    {selectedOptions.length > 1\n                      ? `${selectedOptions.length} selected`\n                      : selectedOptions[0]?.label}\n                  </span>\n                </>\n              )}\n            </Button>\n          </PopoverTrigger>\n          <PopoverContent\n            id={inputListboxId}\n            dir={dir}\n            align=\"start\"\n            className=\"w-48 p-0\"\n          >\n            <Command>\n              <CommandInput placeholder=\"Search options...\" />\n              <CommandList>\n                <CommandEmpty>No options found.</CommandEmpty>\n                <CommandGroup>\n                  {selectOptions.map((option) => {\n                    const isSelected = selectedValues.includes(option.value);\n                    return (\n                      <CommandItem\n                        key={option.value}\n                        value={option.value}\n                        onSelect={() => {\n                          const newValues = isSelected\n                            ? selectedValues.filter((v) => v !== option.value)\n                            : [...selectedValues, option.value];\n                          onValueChange(\n                            newValues.length > 0 ? newValues : undefined,\n                          );\n                        }}\n                      >\n                        {option.icon && <option.icon />}\n                        <span className=\"truncate\">{option.label}</span>\n                        {option.count && (\n                          <span className=\"ms-auto font-mono text-xs\">\n                            {option.count}\n                          </span>\n                        )}\n                        <Check\n                          className={cn(\n                            \"ms-auto\",\n                            isSelected ? \"opacity-100\" : \"opacity-0\",\n                          )}\n                        />\n                      </CommandItem>\n                    );\n                  })}\n                </CommandGroup>\n              </CommandList>\n            </Command>\n          </PopoverContent>\n        </Popover>\n      );\n    }\n\n    const selectedOption = selectOptions.find(\n      (opt) => opt.value === (value as string),\n    );\n\n    return (\n      <Popover open={showValueSelector} onOpenChange={setShowValueSelector}>\n        <PopoverTrigger asChild>\n          <Button\n            id={inputId}\n            aria-controls={inputListboxId}\n            dir={dir}\n            variant=\"outline\"\n            className=\"h-8 w-full justify-start rounded font-normal\"\n          >\n            {selectedOption ? (\n              <>\n                {selectedOption.icon && <selectedOption.icon />}\n                <span className=\"truncate\">{selectedOption.label}</span>\n              </>\n            ) : (\n              <span className=\"text-muted-foreground\">{placeholder}</span>\n            )}\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent\n          id={inputListboxId}\n          dir={dir}\n          align=\"start\"\n          className=\"w-[200px] p-0\"\n        >\n          <Command>\n            <CommandInput placeholder=\"Search options...\" />\n            <CommandList>\n              <CommandEmpty>No options found.</CommandEmpty>\n              <CommandGroup>\n                {selectOptions.map((option) => (\n                  <CommandItem\n                    key={option.value}\n                    value={option.value}\n                    onSelect={() => {\n                      onValueChange(option.value);\n                      setShowValueSelector(false);\n                    }}\n                  >\n                    {option.icon && <option.icon />}\n                    <span className=\"truncate\">{option.label}</span>\n                    {option.count && (\n                      <span className=\"ms-auto font-mono text-xs\">\n                        {option.count}\n                      </span>\n                    )}\n                    <Check\n                      className={cn(\n                        \"ms-auto\",\n                        value === option.value ? \"opacity-100\" : \"opacity-0\",\n                      )}\n                    />\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n    );\n  }\n\n  if (isBetween) {\n    return (\n      <div className=\"flex gap-2\">\n        <Input\n          id={inputId}\n          type=\"text\"\n          placeholder=\"Start\"\n          className=\"h-8 w-full flex-1 rounded\"\n          value={(localValue as string | undefined) ?? \"\"}\n          onChange={(event) => {\n            const val = event.target.value;\n            const newValue = val === \"\" ? undefined : val;\n            setLocalValue(newValue);\n            debouncedOnChange(newValue);\n          }}\n        />\n        <Input\n          id={`${inputId}-end`}\n          type=\"text\"\n          placeholder=\"End\"\n          className=\"h-8 w-full flex-1 rounded\"\n          value={(localEndValue as string | undefined) ?? \"\"}\n          onChange={(event) => {\n            const val = event.target.value;\n            const newValue = val === \"\" ? undefined : val;\n            setLocalEndValue(newValue);\n            debouncedOnEndValueChange(newValue);\n          }}\n        />\n      </div>\n    );\n  }\n\n  return (\n    <Input\n      id={inputId}\n      type=\"text\"\n      placeholder={placeholder}\n      className=\"h-8 w-full rounded\"\n      value={(localValue as string | undefined) ?? \"\"}\n      onChange={(event) => {\n        const val = event.target.value;\n        const newValue = val === \"\" ? undefined : val;\n        setLocalValue(newValue);\n        debouncedOnChange(newValue);\n      }}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "src/components/data-grid/data-grid-filter-menu.tsx"
    },
    {
      "path": "src/components/ui/direction.tsx",
      "content": "\"use client\";\n\nimport { Direction } from \"radix-ui\";\nimport type * as React from \"react\";\n\nfunction DirectionProvider({\n  dir,\n  direction,\n  children,\n}: React.ComponentProps<typeof Direction.DirectionProvider> & {\n  direction?: React.ComponentProps<typeof Direction.DirectionProvider>[\"dir\"];\n}) {\n  return (\n    <Direction.DirectionProvider dir={direction ?? dir}>\n      {children}\n    </Direction.DirectionProvider>\n  );\n}\n\nconst useDirection = Direction.useDirection;\n\nexport { DirectionProvider, useDirection };\n",
      "type": "registry:ui",
      "target": "src/components/ui/direction.tsx"
    },
    {
      "path": "src/components/ui/sortable.tsx",
      "content": "\"use client\";\n\nimport {\n  type Announcements,\n  closestCenter,\n  closestCorners,\n  DndContext,\n  type DndContextProps,\n  type DragEndEvent,\n  type DraggableAttributes,\n  type DraggableSyntheticListeners,\n  DragOverlay,\n  type DragStartEvent,\n  type DropAnimation,\n  defaultDropAnimationSideEffects,\n  KeyboardSensor,\n  MouseSensor,\n  type ScreenReaderInstructions,\n  TouchSensor,\n  type UniqueIdentifier,\n  useSensor,\n  useSensors,\n} from \"@dnd-kit/core\";\nimport {\n  restrictToHorizontalAxis,\n  restrictToParentElement,\n  restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n  arrayMove,\n  horizontalListSortingStrategy,\n  SortableContext,\n  type SortableContextProps,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\n\nconst orientationConfig = {\n  vertical: {\n    modifiers: [restrictToVerticalAxis, restrictToParentElement],\n    strategy: verticalListSortingStrategy,\n    collisionDetection: closestCenter,\n  },\n  horizontal: {\n    modifiers: [restrictToHorizontalAxis, restrictToParentElement],\n    strategy: horizontalListSortingStrategy,\n    collisionDetection: closestCenter,\n  },\n  mixed: {\n    modifiers: [restrictToParentElement],\n    strategy: undefined,\n    collisionDetection: closestCorners,\n  },\n};\n\nconst ROOT_NAME = \"Sortable\";\nconst CONTENT_NAME = \"SortableContent\";\nconst ITEM_NAME = \"SortableItem\";\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\nconst OVERLAY_NAME = \"SortableOverlay\";\n\ninterface SortableRootContextValue<T> {\n  id: string;\n  items: UniqueIdentifier[];\n  modifiers: DndContextProps[\"modifiers\"];\n  strategy: SortableContextProps[\"strategy\"];\n  activeId: UniqueIdentifier | null;\n  setActiveId: (id: UniqueIdentifier | null) => void;\n  getItemValue: (item: T) => UniqueIdentifier;\n  flatCursor: boolean;\n}\n\nconst SortableRootContext =\n  React.createContext<SortableRootContextValue<unknown> | null>(null);\n\nfunction useSortableContext(consumerName: string) {\n  const context = React.useContext(SortableRootContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface GetItemValue<T> {\n  /**\n   * Callback that returns a unique identifier for each sortable item. Required for array of objects.\n   * @example getItemValue={(item) => item.id}\n   */\n  getItemValue: (item: T) => UniqueIdentifier;\n}\n\ntype SortableProps<T> = DndContextProps &\n  (T extends object ? GetItemValue<T> : Partial<GetItemValue<T>>) & {\n    value: T[];\n    onValueChange?: (items: T[]) => void;\n    onMove?: (\n      event: DragEndEvent & { activeIndex: number; overIndex: number },\n    ) => void;\n    strategy?: SortableContextProps[\"strategy\"];\n    orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\n    flatCursor?: boolean;\n  };\n\nfunction Sortable<T>(props: SortableProps<T>) {\n  const {\n    value,\n    onValueChange,\n    collisionDetection,\n    modifiers,\n    strategy,\n    onMove,\n    orientation = \"vertical\",\n    flatCursor = false,\n    getItemValue: getItemValueProp,\n    accessibility,\n    ...sortableProps\n  } = props;\n\n  const id = React.useId();\n  const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);\n\n  const sensors = useSensors(\n    useSensor(MouseSensor),\n    useSensor(TouchSensor),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    }),\n  );\n  const config = React.useMemo(\n    () => orientationConfig[orientation],\n    [orientation],\n  );\n\n  const getItemValue = React.useCallback(\n    (item: T): UniqueIdentifier => {\n      if (typeof item === \"object\" && !getItemValueProp) {\n        throw new Error(\n          \"`getItemValue` is required when using array of objects\",\n        );\n      }\n      return getItemValueProp\n        ? getItemValueProp(item)\n        : (item as UniqueIdentifier);\n    },\n    [getItemValueProp],\n  );\n\n  const items = React.useMemo(() => {\n    return value.map((item) => getItemValue(item));\n  }, [value, getItemValue]);\n\n  const onDragStart = React.useCallback(\n    (event: DragStartEvent) => {\n      sortableProps.onDragStart?.(event);\n\n      if (event.activatorEvent.defaultPrevented) return;\n\n      setActiveId(event.active.id);\n    },\n    [sortableProps.onDragStart],\n  );\n\n  const onDragEnd = React.useCallback(\n    (event: DragEndEvent) => {\n      sortableProps.onDragEnd?.(event);\n\n      if (event.activatorEvent.defaultPrevented) return;\n\n      const { active, over } = event;\n      if (over && active.id !== over?.id) {\n        const activeIndex = value.findIndex(\n          (item) => getItemValue(item) === active.id,\n        );\n        const overIndex = value.findIndex(\n          (item) => getItemValue(item) === over.id,\n        );\n\n        if (onMove) {\n          onMove({ ...event, activeIndex, overIndex });\n        } else {\n          onValueChange?.(arrayMove(value, activeIndex, overIndex));\n        }\n      }\n      setActiveId(null);\n    },\n    [value, onValueChange, onMove, getItemValue, sortableProps.onDragEnd],\n  );\n\n  const onDragCancel = React.useCallback(\n    (event: DragEndEvent) => {\n      sortableProps.onDragCancel?.(event);\n\n      if (event.activatorEvent.defaultPrevented) return;\n\n      setActiveId(null);\n    },\n    [sortableProps.onDragCancel],\n  );\n\n  const announcements: Announcements = React.useMemo(\n    () => ({\n      onDragStart({ active }) {\n        const activeValue = active.id.toString();\n        return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\n      },\n      onDragOver({ active, over }) {\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          const activeIndex = active.data.current?.sortable.index ?? 0;\n          const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n          const activeValue = active.id.toString();\n          return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n        }\n        return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n      },\n      onDragEnd({ active, over }) {\n        const activeValue = active.id.toString();\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\n        }\n        return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\n      },\n      onDragCancel({ active }) {\n        const activeIndex = active.data.current?.sortable.index ?? 0;\n        const activeValue = active.id.toString();\n        return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\n      },\n      onDragMove({ active, over }) {\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          const activeIndex = active.data.current?.sortable.index ?? 0;\n          const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n          const activeValue = active.id.toString();\n          return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n        }\n        return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n      },\n    }),\n    [value],\n  );\n\n  const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\n    () => ({\n      draggable: `\n        To pick up a sortable item, press space or enter.\n        While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\n        Press space or enter again to drop the item in its new position, or press escape to cancel.\n      `,\n    }),\n    [orientation],\n  );\n\n  const contextValue = React.useMemo(\n    () => ({\n      id,\n      items,\n      modifiers: modifiers ?? config.modifiers,\n      strategy: strategy ?? config.strategy,\n      activeId,\n      setActiveId,\n      getItemValue,\n      flatCursor,\n    }),\n    [\n      id,\n      items,\n      modifiers,\n      strategy,\n      config.modifiers,\n      config.strategy,\n      activeId,\n      getItemValue,\n      flatCursor,\n    ],\n  );\n\n  return (\n    <SortableRootContext.Provider\n      value={contextValue as SortableRootContextValue<unknown>}\n    >\n      <DndContext\n        collisionDetection={collisionDetection ?? config.collisionDetection}\n        modifiers={modifiers ?? config.modifiers}\n        sensors={sensors}\n        {...sortableProps}\n        id={id}\n        onDragStart={onDragStart}\n        onDragEnd={onDragEnd}\n        onDragCancel={onDragCancel}\n        accessibility={{\n          announcements,\n          screenReaderInstructions,\n          ...accessibility,\n        }}\n      />\n    </SortableRootContext.Provider>\n  );\n}\n\nconst SortableContentContext = React.createContext<boolean>(false);\n\ninterface SortableContentProps extends React.ComponentProps<\"div\"> {\n  strategy?: SortableContextProps[\"strategy\"];\n  children: React.ReactNode;\n  asChild?: boolean;\n  withoutSlot?: boolean;\n}\n\nfunction SortableContent(props: SortableContentProps) {\n  const {\n    strategy: strategyProp,\n    asChild,\n    withoutSlot,\n    children,\n    ref,\n    ...contentProps\n  } = props;\n\n  const context = useSortableContext(CONTENT_NAME);\n\n  const ContentPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SortableContentContext.Provider value={true}>\n      <SortableContext\n        items={context.items}\n        strategy={strategyProp ?? context.strategy}\n      >\n        {withoutSlot ? (\n          children\n        ) : (\n          <ContentPrimitive\n            data-slot=\"sortable-content\"\n            {...contentProps}\n            ref={ref}\n          >\n            {children}\n          </ContentPrimitive>\n        )}\n      </SortableContext>\n    </SortableContentContext.Provider>\n  );\n}\n\ninterface SortableItemContextValue {\n  id: string;\n  attributes: DraggableAttributes;\n  listeners: DraggableSyntheticListeners | undefined;\n  setActivatorNodeRef: (node: HTMLElement | null) => void;\n  isDragging?: boolean;\n  disabled?: boolean;\n}\n\nconst SortableItemContext =\n  React.createContext<SortableItemContextValue | null>(null);\n\nfunction useSortableItemContext(consumerName: string) {\n  const context = React.useContext(SortableItemContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n  }\n  return context;\n}\n\ninterface SortableItemProps extends React.ComponentProps<\"div\"> {\n  value: UniqueIdentifier;\n  asHandle?: boolean;\n  asChild?: boolean;\n  disabled?: boolean;\n}\n\nfunction SortableItem(props: SortableItemProps) {\n  const {\n    value,\n    style,\n    asHandle,\n    asChild,\n    disabled,\n    className,\n    ref,\n    ...itemProps\n  } = props;\n\n  const inSortableContent = React.useContext(SortableContentContext);\n  const inSortableOverlay = React.useContext(SortableOverlayContext);\n\n  if (!inSortableContent && !inSortableOverlay) {\n    throw new Error(\n      `\\`${ITEM_NAME}\\` must be used within \\`${CONTENT_NAME}\\` or \\`${OVERLAY_NAME}\\``,\n    );\n  }\n\n  if (value === \"\") {\n    throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\n  }\n\n  const context = useSortableContext(ITEM_NAME);\n  const id = React.useId();\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    setActivatorNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({ id: value, disabled });\n\n  const composedRef = useComposedRefs(ref, (node) => {\n    if (disabled) return;\n    setNodeRef(node);\n    if (asHandle) setActivatorNodeRef(node);\n  });\n\n  const composedStyle = React.useMemo<React.CSSProperties>(() => {\n    return {\n      transform: CSS.Translate.toString(transform),\n      transition,\n      ...style,\n    };\n  }, [transform, transition, style]);\n\n  const itemContext = React.useMemo<SortableItemContextValue>(\n    () => ({\n      id,\n      attributes,\n      listeners,\n      setActivatorNodeRef,\n      isDragging,\n      disabled,\n    }),\n    [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\n  );\n\n  const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SortableItemContext.Provider value={itemContext}>\n      <ItemPrimitive\n        id={id}\n        data-disabled={disabled}\n        data-dragging={isDragging ? \"\" : undefined}\n        data-slot=\"sortable-item\"\n        {...itemProps}\n        {...(asHandle && !disabled ? attributes : {})}\n        {...(asHandle && !disabled ? listeners : {})}\n        ref={composedRef}\n        style={composedStyle}\n        className={cn(\n          \"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1\",\n          {\n            \"touch-none select-none\": asHandle,\n            \"cursor-default\": context.flatCursor,\n            \"data-dragging:cursor-grabbing\": !context.flatCursor,\n            \"cursor-grab\": !isDragging && asHandle && !context.flatCursor,\n            \"opacity-50\": isDragging,\n            \"pointer-events-none opacity-50\": disabled,\n          },\n          className,\n        )}\n      />\n    </SortableItemContext.Provider>\n  );\n}\n\ninterface SortableItemHandleProps extends React.ComponentProps<\"button\"> {\n  asChild?: boolean;\n}\n\nfunction SortableItemHandle(props: SortableItemHandleProps) {\n  const { asChild, disabled, className, ref, ...itemHandleProps } = props;\n\n  const context = useSortableContext(ITEM_HANDLE_NAME);\n  const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);\n\n  const isDisabled = disabled ?? itemContext.disabled;\n\n  const composedRef = useComposedRefs(ref, (node) => {\n    if (!isDisabled) return;\n    itemContext.setActivatorNodeRef(node);\n  });\n\n  const HandlePrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n  return (\n    <HandlePrimitive\n      type=\"button\"\n      aria-controls={itemContext.id}\n      data-disabled={isDisabled}\n      data-dragging={itemContext.isDragging ? \"\" : undefined}\n      data-slot=\"sortable-item-handle\"\n      {...itemHandleProps}\n      {...(isDisabled ? {} : itemContext.attributes)}\n      {...(isDisabled ? {} : itemContext.listeners)}\n      ref={composedRef}\n      className={cn(\n        \"select-none disabled:pointer-events-none disabled:opacity-50\",\n        context.flatCursor\n          ? \"cursor-default\"\n          : \"cursor-grab data-dragging:cursor-grabbing\",\n        className,\n      )}\n      disabled={isDisabled}\n    />\n  );\n}\n\nconst SortableOverlayContext = React.createContext(false);\n\nconst dropAnimation: DropAnimation = {\n  sideEffects: defaultDropAnimationSideEffects({\n    styles: {\n      active: {\n        opacity: \"0.4\",\n      },\n    },\n  }),\n};\n\ninterface SortableOverlayProps\n  extends Omit<React.ComponentProps<typeof DragOverlay>, \"children\"> {\n  container?: Element | DocumentFragment | null;\n  children?:\n    | ((params: { value: UniqueIdentifier }) => React.ReactNode)\n    | React.ReactNode;\n}\n\nfunction SortableOverlay(props: SortableOverlayProps) {\n  const { container: containerProp, children, ...overlayProps } = props;\n\n  const context = useSortableContext(OVERLAY_NAME);\n\n  const [mounted, setMounted] = React.useState(false);\n\n  React.useLayoutEffect(() => setMounted(true), []);\n\n  const container =\n    containerProp ?? (mounted ? globalThis.document?.body : null);\n\n  if (!container) return null;\n\n  return ReactDOM.createPortal(\n    <DragOverlay\n      dropAnimation={dropAnimation}\n      modifiers={context.modifiers}\n      className={cn(!context.flatCursor && \"cursor-grabbing\")}\n      {...overlayProps}\n    >\n      <SortableOverlayContext.Provider value={true}>\n        {context.activeId\n          ? typeof children === \"function\"\n            ? children({ value: context.activeId })\n            : children\n          : null}\n      </SortableOverlayContext.Provider>\n    </DragOverlay>,\n    container,\n  );\n}\n\nexport {\n  Sortable,\n  SortableContent,\n  SortableItem,\n  SortableItemHandle,\n  SortableOverlay,\n  type SortableProps,\n};\n",
      "type": "registry:ui",
      "target": "src/components/ui/sortable.tsx"
    },
    {
      "path": "src/hooks/use-debounced-callback.ts",
      "content": "/**\n * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts\n */\n\nimport * as React from \"react\";\n\nimport { useCallbackRef } from \"@/hooks/use-callback-ref\";\n\nexport function useDebouncedCallback<T extends (...args: never[]) => unknown>(\n  callback: T,\n  delay: number,\n) {\n  const handleCallback = useCallbackRef(callback);\n  const debounceTimerRef = React.useRef(0);\n  React.useEffect(\n    () => () => window.clearTimeout(debounceTimerRef.current),\n    [],\n  );\n\n  const setValue = React.useCallback(\n    (...args: Parameters<T>) => {\n      window.clearTimeout(debounceTimerRef.current);\n      debounceTimerRef.current = window.setTimeout(\n        () => handleCallback(...args),\n        delay,\n      );\n    },\n    [handleCallback, delay],\n  );\n\n  return setValue;\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "src/lib/compose-refs.ts",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // biome-ignore lint/correctness/useExhaustiveDependencies: we don't want to re-run this callback when the refs change\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/data-grid-filters.ts",
      "content": "import type { FilterFn, Row } from \"@tanstack/react-table\";\nimport type {\n  BooleanFilterOperator,\n  DateFilterOperator,\n  FilterOperator,\n  FilterValue,\n  NumberFilterOperator,\n  SelectFilterOperator,\n  TextFilterOperator,\n} from \"@/types/data-grid\";\n\nexport const TEXT_FILTER_OPERATORS: ReadonlyArray<{\n  label: string;\n  value: TextFilterOperator;\n}> = [\n  { label: \"Contains\", value: \"contains\" },\n  { label: \"Does not contain\", value: \"notContains\" },\n  { label: \"Is\", value: \"equals\" },\n  { label: \"Is not\", value: \"notEquals\" },\n  { label: \"Starts with\", value: \"startsWith\" },\n  { label: \"Ends with\", value: \"endsWith\" },\n  { label: \"Is empty\", value: \"isEmpty\" },\n  { label: \"Is not empty\", value: \"isNotEmpty\" },\n];\n\nexport const NUMBER_FILTER_OPERATORS: ReadonlyArray<{\n  label: string;\n  value: NumberFilterOperator;\n}> = [\n  { label: \"Is\", value: \"equals\" },\n  { label: \"Is not\", value: \"notEquals\" },\n  { label: \"Is less than\", value: \"lessThan\" },\n  { label: \"Is less than or equal to\", value: \"lessThanOrEqual\" },\n  { label: \"Is greater than\", value: \"greaterThan\" },\n  { label: \"Is greater than or equal to\", value: \"greaterThanOrEqual\" },\n  { label: \"Is between\", value: \"isBetween\" },\n  { label: \"Is empty\", value: \"isEmpty\" },\n  { label: \"Is not empty\", value: \"isNotEmpty\" },\n];\n\nexport const DATE_FILTER_OPERATORS: ReadonlyArray<{\n  label: string;\n  value: DateFilterOperator;\n}> = [\n  { label: \"Is\", value: \"equals\" },\n  { label: \"Is not\", value: \"notEquals\" },\n  { label: \"Is before\", value: \"before\" },\n  { label: \"Is after\", value: \"after\" },\n  { label: \"Is on or before\", value: \"onOrBefore\" },\n  { label: \"Is on or after\", value: \"onOrAfter\" },\n  { label: \"Is between\", value: \"isBetween\" },\n  { label: \"Is empty\", value: \"isEmpty\" },\n  { label: \"Is not empty\", value: \"isNotEmpty\" },\n];\n\nexport const SELECT_FILTER_OPERATORS: ReadonlyArray<{\n  label: string;\n  value: SelectFilterOperator;\n}> = [\n  { label: \"Is\", value: \"is\" },\n  { label: \"Is not\", value: \"isNot\" },\n  { label: \"Has any of\", value: \"isAnyOf\" },\n  { label: \"Has none of\", value: \"isNoneOf\" },\n  { label: \"Is empty\", value: \"isEmpty\" },\n  { label: \"Is not empty\", value: \"isNotEmpty\" },\n];\n\nexport const BOOLEAN_FILTER_OPERATORS: ReadonlyArray<{\n  label: string;\n  value: BooleanFilterOperator;\n}> = [\n  { label: \"Is\", value: \"isTrue\" },\n  { label: \"Is not\", value: \"isFalse\" },\n];\n\nexport function getDefaultOperator(variant: string): FilterOperator {\n  switch (variant) {\n    case \"number\":\n      return \"equals\";\n    case \"date\":\n      return \"equals\";\n    case \"select\":\n    case \"multi-select\":\n      return \"is\";\n    case \"checkbox\":\n      return \"isTrue\";\n    default:\n      return \"contains\";\n  }\n}\n\nexport function getOperatorsForVariant(variant: string): ReadonlyArray<{\n  label: string;\n  value: FilterOperator;\n}> {\n  switch (variant) {\n    case \"number\":\n      return NUMBER_FILTER_OPERATORS;\n    case \"date\":\n      return DATE_FILTER_OPERATORS;\n    case \"select\":\n    case \"multi-select\":\n      return SELECT_FILTER_OPERATORS;\n    case \"checkbox\":\n      return BOOLEAN_FILTER_OPERATORS;\n    default:\n      return TEXT_FILTER_OPERATORS;\n  }\n}\n\nexport function getFilterFn<TData>(): FilterFn<TData> {\n  return (row: Row<TData>, columnId: string, filterValue: unknown): boolean => {\n    if (!filterValue || typeof filterValue !== \"object\") {\n      return true;\n    }\n\n    const filter = filterValue as FilterValue;\n    const { operator, value, endValue } = filter;\n\n    const cellValue = row.getValue(columnId);\n\n    if (operator === \"isEmpty\") {\n      return (\n        cellValue === null ||\n        cellValue === undefined ||\n        cellValue === \"\" ||\n        (Array.isArray(cellValue) && cellValue.length === 0)\n      );\n    }\n\n    if (operator === \"isNotEmpty\") {\n      return !(\n        cellValue === null ||\n        cellValue === undefined ||\n        cellValue === \"\" ||\n        (Array.isArray(cellValue) && cellValue.length === 0)\n      );\n    }\n\n    if (operator === \"isTrue\") {\n      return cellValue === true;\n    }\n\n    if (operator === \"isFalse\") {\n      return cellValue === false || !cellValue;\n    }\n\n    if (value === undefined || value === null || value === \"\") {\n      return true;\n    }\n\n    const cellValueStr = String(cellValue ?? \"\").toLowerCase();\n    const filterValueStr =\n      typeof value === \"string\" ? value.toLowerCase() : String(value);\n\n    if (operator === \"contains\") {\n      return cellValueStr.includes(filterValueStr);\n    }\n\n    if (operator === \"notContains\") {\n      return !cellValueStr.includes(filterValueStr);\n    }\n\n    if (operator === \"equals\") {\n      if (typeof cellValue === \"number\" && typeof value === \"number\") {\n        return cellValue === value;\n      }\n      if (cellValue instanceof Date && typeof value === \"string\") {\n        const cellDate = new Date(cellValue);\n        const filterDate = new Date(value);\n        return cellDate.toDateString() === filterDate.toDateString();\n      }\n      return cellValueStr === filterValueStr;\n    }\n\n    if (operator === \"notEquals\") {\n      if (typeof cellValue === \"number\" && typeof value === \"number\") {\n        return cellValue !== value;\n      }\n      if (cellValue instanceof Date && typeof value === \"string\") {\n        const cellDate = new Date(cellValue);\n        const filterDate = new Date(value);\n        return cellDate.toDateString() !== filterDate.toDateString();\n      }\n      return cellValueStr !== filterValueStr;\n    }\n\n    if (operator === \"startsWith\") {\n      return cellValueStr.startsWith(filterValueStr);\n    }\n\n    if (operator === \"endsWith\") {\n      return cellValueStr.endsWith(filterValueStr);\n    }\n\n    if (typeof cellValue === \"number\" && typeof value === \"number\") {\n      if (operator === \"greaterThan\") {\n        return cellValue > value;\n      }\n\n      if (operator === \"greaterThanOrEqual\") {\n        return cellValue >= value;\n      }\n\n      if (operator === \"lessThan\") {\n        return cellValue < value;\n      }\n\n      if (operator === \"lessThanOrEqual\") {\n        return cellValue <= value;\n      }\n\n      if (operator === \"isBetween\" && typeof endValue === \"number\") {\n        return cellValue >= value && cellValue <= endValue;\n      }\n    }\n\n    if (cellValue instanceof Date || typeof cellValue === \"string\") {\n      const cellDate = new Date(cellValue);\n      if (!Number.isNaN(cellDate.getTime()) && typeof value === \"string\") {\n        const filterDate = new Date(value);\n\n        if (operator === \"before\") {\n          return cellDate < filterDate;\n        }\n\n        if (operator === \"after\") {\n          return cellDate > filterDate;\n        }\n\n        if (operator === \"onOrBefore\") {\n          return cellDate <= filterDate;\n        }\n\n        if (operator === \"onOrAfter\") {\n          return cellDate >= filterDate;\n        }\n\n        if (operator === \"isBetween\" && typeof endValue === \"string\") {\n          const filterDate2 = new Date(endValue);\n          return cellDate >= filterDate && cellDate <= filterDate2;\n        }\n      }\n    }\n\n    if (operator === \"is\") {\n      if (Array.isArray(cellValue)) {\n        return cellValue.some((v) => String(v) === String(value));\n      }\n      return String(cellValue) === String(value);\n    }\n\n    if (operator === \"isNot\") {\n      if (Array.isArray(cellValue)) {\n        return !cellValue.some((v) => String(v) === String(value));\n      }\n      return String(cellValue) !== String(value);\n    }\n\n    if (operator === \"isAnyOf\" && Array.isArray(value)) {\n      if (Array.isArray(cellValue)) {\n        return cellValue.some((v) =>\n          value.some((fv) => String(v) === String(fv)),\n        );\n      }\n      return value.some((fv) => String(cellValue) === String(fv));\n    }\n\n    if (operator === \"isNoneOf\" && Array.isArray(value)) {\n      if (Array.isArray(cellValue)) {\n        return !cellValue.some((v) =>\n          value.some((fv) => String(v) === String(fv)),\n        );\n      }\n      return !value.some((fv) => String(cellValue) === String(fv));\n    }\n\n    return true;\n  };\n}\n",
      "type": "registry:lib",
      "target": "src/lib/data-grid-filters.ts"
    },
    {
      "path": "src/lib/format.ts",
      "content": "export function formatDate(\n  date: Date | string | number | undefined,\n  opts: Intl.DateTimeFormatOptions = {},\n) {\n  if (!date) return \"\";\n\n  try {\n    return new Intl.DateTimeFormat(\"en-US\", {\n      month: opts.month ?? \"long\",\n      day: opts.day ?? \"numeric\",\n      year: opts.year ?? \"numeric\",\n      ...opts,\n    }).format(new Date(date));\n  } catch (_err) {\n    return \"\";\n  }\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/types/data-grid.ts",
      "content": "import type { Cell, RowData, TableMeta } from \"@tanstack/react-table\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n  label: string;\n  value: string;\n  icon?: React.ComponentType<React.ComponentProps<\"svg\">>;\n  count?: number;\n}\n\nexport type CellOpts =\n  | {\n      variant: \"short-text\";\n    }\n  | {\n      variant: \"long-text\";\n    }\n  | {\n      variant: \"number\";\n      min?: number;\n      max?: number;\n      step?: number;\n    }\n  | {\n      variant: \"select\";\n      options: CellSelectOption[];\n    }\n  | {\n      variant: \"multi-select\";\n      options: CellSelectOption[];\n    }\n  | {\n      variant: \"checkbox\";\n    }\n  | {\n      variant: \"date\";\n    }\n  | {\n      variant: \"url\";\n    }\n  | {\n      variant: \"file\";\n      maxFileSize?: number;\n      maxFiles?: number;\n      accept?: string;\n      multiple?: boolean;\n    };\n\nexport interface CellUpdate {\n  rowIndex: number;\n  columnId: string;\n  value: unknown;\n}\n\ndeclare module \"@tanstack/react-table\" {\n  interface ColumnMeta<TData extends RowData, TValue> {\n    label?: string;\n    cell?: CellOpts;\n  }\n\n  interface TableMeta<TData extends RowData> {\n    dataGridRef?: React.RefObject<HTMLElement | null>;\n    cellMapRef?: React.RefObject<Map<string, HTMLDivElement>>;\n    focusedCell?: CellPosition | null;\n    editingCell?: CellPosition | null;\n    selectionState?: SelectionState;\n    searchOpen?: boolean;\n    getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n    getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n    getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n    getVisualRowIndex?: (rowId: string) => number | undefined;\n    scrollToCell?: (\n      rowIndex: number,\n      columnId: string,\n      align?: \"auto\" | \"start\" | \"center\" | \"end\",\n    ) => void;\n    rowHeight?: RowHeightValue;\n    onRowHeightChange?: (value: RowHeightValue) => void;\n    onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n    onDataUpdate?: (params: CellUpdate | Array<CellUpdate>) => void;\n    onRowsDelete?: (rowIndices: number[]) => void | Promise<void>;\n    onColumnClick?: (columnId: string) => void;\n    onCellClick?: (\n      rowIndex: number,\n      columnId: string,\n      event?: React.MouseEvent,\n    ) => void;\n    onCellDoubleClick?: (rowIndex: number, columnId: string) => void;\n    onCellMouseDown?: (\n      rowIndex: number,\n      columnId: string,\n      event: React.MouseEvent,\n    ) => void;\n    onCellMouseEnter?: (rowIndex: number, columnId: string) => void;\n    onCellMouseUp?: () => void;\n    onCellContextMenu?: (\n      rowIndex: number,\n      columnId: string,\n      event: React.MouseEvent,\n    ) => void;\n    onCellEditingStart?: (rowIndex: number, columnId: string) => void;\n    onCellEditingStop?: (opts?: {\n      direction?: NavigationDirection;\n      moveToNextRow?: boolean;\n    }) => void;\n    onCellsCopy?: () => void;\n    onCellsCut?: () => void;\n    onCellsPaste?: (expand?: boolean) => void;\n    onSelectionClear?: () => void;\n    onFilesUpload?: (params: {\n      files: File[];\n      rowIndex: number;\n      columnId: string;\n    }) => Promise<FileCellData[]>;\n    onFilesDelete?: (params: {\n      fileIds: string[];\n      rowIndex: number;\n      columnId: string;\n    }) => void | Promise<void>;\n    contextMenu?: ContextMenuState;\n    onContextMenuOpenChange?: (open: boolean) => void;\n    pasteDialog?: PasteDialogState;\n    onPasteDialogOpenChange?: (open: boolean) => void;\n    readOnly?: boolean;\n  }\n}\n\nexport interface CellPosition {\n  rowIndex: number;\n  columnId: string;\n}\n\nexport interface CellRange {\n  start: CellPosition;\n  end: CellPosition;\n}\n\nexport interface SelectionState {\n  selectedCells: Set<string>;\n  selectionRange: CellRange | null;\n  isSelecting: boolean;\n}\n\nexport interface ContextMenuState {\n  open: boolean;\n  x: number;\n  y: number;\n}\n\nexport interface PasteDialogState {\n  open: boolean;\n  rowsNeeded: number;\n  clipboardText: string;\n}\n\nexport type NavigationDirection =\n  | \"up\"\n  | \"down\"\n  | \"left\"\n  | \"right\"\n  | \"home\"\n  | \"end\"\n  | \"ctrl+up\"\n  | \"ctrl+down\"\n  | \"ctrl+home\"\n  | \"ctrl+end\"\n  | \"pageup\"\n  | \"pagedown\"\n  | \"pageleft\"\n  | \"pageright\";\n\nexport interface SearchState {\n  searchMatches: CellPosition[];\n  matchIndex: number;\n  searchOpen: boolean;\n  onSearchOpenChange: (open: boolean) => void;\n  searchQuery: string;\n  onSearchQueryChange: (query: string) => void;\n  onSearch: (query: string) => void;\n  onNavigateToNextMatch: () => void;\n  onNavigateToPrevMatch: () => void;\n}\n\nexport interface DataGridCellProps<TData> {\n  cell: Cell<TData, unknown>;\n  tableMeta: TableMeta<TData>;\n  rowIndex: number;\n  columnId: string;\n  rowHeight: RowHeightValue;\n  isEditing: boolean;\n  isFocused: boolean;\n  isSelected: boolean;\n  isSearchMatch: boolean;\n  isActiveSearchMatch: boolean;\n  readOnly: boolean;\n}\n\nexport interface FileCellData {\n  id: string;\n  name: string;\n  size: number;\n  type: string;\n  url?: string;\n}\n\nexport type TextFilterOperator =\n  | \"contains\"\n  | \"notContains\"\n  | \"equals\"\n  | \"notEquals\"\n  | \"startsWith\"\n  | \"endsWith\"\n  | \"isEmpty\"\n  | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n  | \"equals\"\n  | \"notEquals\"\n  | \"lessThan\"\n  | \"lessThanOrEqual\"\n  | \"greaterThan\"\n  | \"greaterThanOrEqual\"\n  | \"isBetween\"\n  | \"isEmpty\"\n  | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n  | \"equals\"\n  | \"notEquals\"\n  | \"before\"\n  | \"after\"\n  | \"onOrBefore\"\n  | \"onOrAfter\"\n  | \"isBetween\"\n  | \"isEmpty\"\n  | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n  | \"is\"\n  | \"isNot\"\n  | \"isAnyOf\"\n  | \"isNoneOf\"\n  | \"isEmpty\"\n  | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n  | TextFilterOperator\n  | NumberFilterOperator\n  | DateFilterOperator\n  | SelectFilterOperator\n  | BooleanFilterOperator;\n\nexport interface FilterValue {\n  operator: FilterOperator;\n  value?: string | number | string[];\n  endValue?: string | number;\n}\n",
      "type": "registry:file",
      "target": "src/types/data-grid.ts"
    }
  ],
  "type": "registry:component"
}