{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-filter-menu",
  "title": "Data Table Filter Menu",
  "description": "A filter menu component for the data table",
  "dependencies": [
    "@tanstack/react-table",
    "lucide-react",
    "nuqs"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "calendar",
    "command",
    "input",
    "popover",
    "select"
  ],
  "files": [
    {
      "path": "src/components/data-table/data-table-filter-menu.tsx",
      "content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport { BadgeCheck, CalendarIcon, ListFilter, Text, X } from \"lucide-react\";\nimport { useQueryState } from \"nuqs\";\nimport * as React from \"react\";\n\nimport { DataTableRangeFilter } from \"@/components/data-table/data-table-range-filter\";\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 { 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 { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\nimport type { ExtendedColumnFilter, FilterOperator } from \"@/types/data-table\";\n\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst FILTER_SHORTCUT_KEY = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterMenuProps<TData>\n  extends React.ComponentProps<typeof PopoverContent> {\n  table: Table<TData>;\n  debounceMs?: number;\n  throttleMs?: number;\n  shallow?: boolean;\n  disabled?: boolean;\n}\n\nexport function DataTableFilterMenu<TData>({\n  table,\n  debounceMs = DEBOUNCE_MS,\n  throttleMs = THROTTLE_MS,\n  shallow = true,\n  disabled,\n  className,\n  ...props\n}: DataTableFilterMenuProps<TData>) {\n  const id = React.useId();\n\n  const columns = React.useMemo(() => {\n    return table\n      .getAllColumns()\n      .filter((column) => column.columnDef.enableColumnFilter);\n  }, [table]);\n\n  const [open, setOpen] = React.useState(false);\n  const [selectedColumn, setSelectedColumn] =\n    React.useState<Column<TData> | null>(null);\n  const [inputValue, setInputValue] = React.useState(\"\");\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n\n  const onOpenChange = React.useCallback((open: boolean) => {\n    setOpen(open);\n\n    if (!open) {\n      setTimeout(() => {\n        setSelectedColumn(null);\n        setInputValue(\"\");\n      }, 100);\n    }\n  }, []);\n\n  const onInputKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLInputElement>) => {\n      if (\n        REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n        !inputValue &&\n        selectedColumn\n      ) {\n        event.preventDefault();\n        setSelectedColumn(null);\n      }\n    },\n    [inputValue, selectedColumn],\n  );\n\n  const [filters, setFilters] = useQueryState(\n    table.options.meta?.queryKeys?.filters ?? \"filters\",\n    getFiltersStateParser<TData>(columns.map((field) => field.id))\n      .withDefault([])\n      .withOptions({\n        clearOnDefault: true,\n        shallow,\n        throttleMs,\n      }),\n  );\n  const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\n\n  const onFilterAdd = React.useCallback(\n    (column: Column<TData>, value: string) => {\n      if (!value.trim() && column.columnDef.meta?.variant !== \"boolean\") {\n        return;\n      }\n\n      const filterValue =\n        column.columnDef.meta?.variant === \"multiSelect\" ? [value] : value;\n\n      const newFilter: ExtendedColumnFilter<TData> = {\n        id: column.id as Extract<keyof TData, string>,\n        value: filterValue,\n        variant: column.columnDef.meta?.variant ?? \"text\",\n        operator: getDefaultFilterOperator(\n          column.columnDef.meta?.variant ?? \"text\",\n        ),\n        filterId: generateId({ length: 8 }),\n      };\n\n      debouncedSetFilters([...filters, newFilter]);\n      setOpen(false);\n\n      setTimeout(() => {\n        setSelectedColumn(null);\n        setInputValue(\"\");\n      }, 100);\n    },\n    [filters, debouncedSetFilters],\n  );\n\n  const onFilterRemove = React.useCallback(\n    (filterId: string) => {\n      const updatedFilters = filters.filter(\n        (filter) => filter.filterId !== filterId,\n      );\n      debouncedSetFilters(updatedFilters);\n      requestAnimationFrame(() => {\n        triggerRef.current?.focus();\n      });\n    },\n    [filters, debouncedSetFilters],\n  );\n\n  const onFilterUpdate = React.useCallback(\n    (\n      filterId: string,\n      updates: Partial<Omit<ExtendedColumnFilter<TData>, \"filterId\">>,\n    ) => {\n      debouncedSetFilters((prevFilters) => {\n        const updatedFilters = prevFilters.map((filter) => {\n          if (filter.filterId === filterId) {\n            return { ...filter, ...updates } as ExtendedColumnFilter<TData>;\n          }\n          return filter;\n        });\n        return updatedFilters;\n      });\n    },\n    [debouncedSetFilters],\n  );\n\n  const onFiltersReset = React.useCallback(() => {\n    debouncedSetFilters([]);\n  }, [debouncedSetFilters]);\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.includes(event.key.toLowerCase()) &&\n        filters.length > 0\n      ) {\n        event.preventDefault();\n        onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n      }\n    },\n    [filters, onFilterRemove],\n  );\n\n  return (\n    <div role=\"list\" className=\"flex flex-wrap items-center gap-2\">\n      {filters.map((filter) => (\n        <DataTableFilterItem\n          key={filter.filterId}\n          filter={filter}\n          filterItemId={`${id}-filter-${filter.filterId}`}\n          columns={columns}\n          onFilterUpdate={onFilterUpdate}\n          onFilterRemove={onFilterRemove}\n        />\n      ))}\n      {filters.length > 0 && (\n        <Button\n          aria-label=\"Reset all filters\"\n          variant=\"outline\"\n          size=\"icon\"\n          className=\"size-8\"\n          onClick={onFiltersReset}\n        >\n          <X />\n        </Button>\n      )}\n      <Popover open={open} onOpenChange={onOpenChange}>\n        <PopoverTrigger asChild>\n          <Button\n            aria-label=\"Open filter command menu\"\n            variant=\"outline\"\n            size={filters.length > 0 ? \"icon\" : \"sm\"}\n            className={cn(filters.length > 0 && \"size-8\", \"h-8 font-normal\")}\n            ref={triggerRef}\n            onKeyDown={onTriggerKeyDown}\n            disabled={disabled}\n          >\n            <ListFilter className=\"text-muted-foreground\" />\n            {filters.length > 0 ? null : \"Filter\"}\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent\n          className={cn(\n            \"w-full max-w-(--radix-popover-content-available-width) p-0\",\n            className,\n          )}\n          {...props}\n        >\n          <Command loop className=\"[&_[cmdk-input-wrapper]_svg]:hidden\">\n            <CommandInput\n              ref={inputRef}\n              placeholder={\n                selectedColumn\n                  ? (selectedColumn.columnDef.meta?.label ?? selectedColumn.id)\n                  : \"Search fields...\"\n              }\n              value={inputValue}\n              onValueChange={setInputValue}\n              onKeyDown={onInputKeyDown}\n            />\n            <CommandList>\n              {selectedColumn ? (\n                <>\n                  {selectedColumn.columnDef.meta?.options && (\n                    <CommandEmpty>No options found.</CommandEmpty>\n                  )}\n                  <FilterValueSelector\n                    column={selectedColumn}\n                    value={inputValue}\n                    onSelect={(value) => onFilterAdd(selectedColumn, value)}\n                  />\n                </>\n              ) : (\n                <>\n                  <CommandEmpty>No fields found.</CommandEmpty>\n                  <CommandGroup>\n                    {columns.map((column) => (\n                      <CommandItem\n                        key={column.id}\n                        value={column.id}\n                        onSelect={() => {\n                          setSelectedColumn(column);\n                          setInputValue(\"\");\n                          requestAnimationFrame(() => {\n                            inputRef.current?.focus();\n                          });\n                        }}\n                      >\n                        {column.columnDef.meta?.icon && (\n                          <column.columnDef.meta.icon />\n                        )}\n                        <span className=\"truncate\">\n                          {column.columnDef.meta?.label ?? column.id}\n                        </span>\n                      </CommandItem>\n                    ))}\n                  </CommandGroup>\n                </>\n              )}\n            </CommandList>\n          </Command>\n        </PopoverContent>\n      </Popover>\n    </div>\n  );\n}\n\ninterface DataTableFilterItemProps<TData> {\n  filter: ExtendedColumnFilter<TData>;\n  filterItemId: string;\n  columns: Column<TData>[];\n  onFilterUpdate: (\n    filterId: string,\n    updates: Partial<Omit<ExtendedColumnFilter<TData>, \"filterId\">>,\n  ) => void;\n  onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataTableFilterItem<TData>({\n  filter,\n  filterItemId,\n  columns,\n  onFilterUpdate,\n  onFilterRemove,\n}: DataTableFilterItemProps<TData>) {\n  {\n    const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n    const [showOperatorSelector, setShowOperatorSelector] =\n      React.useState(false);\n    const [showValueSelector, setShowValueSelector] = React.useState(false);\n\n    const column = columns.find((column) => column.id === filter.id);\n\n    const operatorListboxId = `${filterItemId}-operator-listbox`;\n    const inputId = `${filterItemId}-input`;\n\n    const columnMeta = column?.columnDef.meta;\n    const filterOperators = getFilterOperators(filter.variant);\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 || showValueSelector) {\n          return;\n        }\n\n        if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\n          event.preventDefault();\n          onFilterRemove(filter.filterId);\n        }\n      },\n      [\n        filter.filterId,\n        showFieldSelector,\n        showOperatorSelector,\n        showValueSelector,\n        onFilterRemove,\n      ],\n    );\n\n    if (!column) return null;\n\n    return (\n      <div\n        key={filter.filterId}\n        role=\"listitem\"\n        id={filterItemId}\n        className=\"flex h-8 items-center rounded-md bg-background\"\n        onKeyDown={onItemKeyDown}\n      >\n        <Popover open={showFieldSelector} onOpenChange={setShowFieldSelector}>\n          <PopoverTrigger asChild>\n            <Button\n              variant=\"ghost\"\n              className=\"rounded-none rounded-l-md border border-input border-r-0 font-normal dark:bg-input/30\"\n            >\n              {columnMeta?.icon && (\n                <columnMeta.icon className=\"text-muted-foreground\" />\n              )}\n              {columnMeta?.label ?? column.id}\n            </Button>\n          </PopoverTrigger>\n          <PopoverContent align=\"start\" className=\"w-48 p-0\">\n            <Command loop>\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                      data-checked={column.id === filter.id}\n                      onSelect={() => {\n                        onFilterUpdate(filter.filterId, {\n                          id: column.id as Extract<keyof TData, string>,\n                          variant: column.columnDef.meta?.variant ?? \"text\",\n                          operator: getDefaultFilterOperator(\n                            column.columnDef.meta?.variant ?? \"text\",\n                          ),\n                          value: \"\",\n                        });\n\n                        setShowFieldSelector(false);\n                      }}\n                    >\n                      {column.columnDef.meta?.icon && (\n                        <column.columnDef.meta.icon />\n                      )}\n                      <span className=\"truncate\">\n                        {column.columnDef.meta?.label ?? column.id}\n                      </span>\n                    </CommandItem>\n                  ))}\n                </CommandGroup>\n              </CommandList>\n            </Command>\n          </PopoverContent>\n        </Popover>\n        <Select\n          open={showOperatorSelector}\n          onOpenChange={setShowOperatorSelector}\n          value={filter.operator}\n          onValueChange={(value: FilterOperator) =>\n            onFilterUpdate(filter.filterId, {\n              operator: value,\n              value:\n                value === \"isEmpty\" || value === \"isNotEmpty\"\n                  ? \"\"\n                  : filter.value,\n            })\n          }\n        >\n          <SelectTrigger\n            aria-controls={operatorListboxId}\n            className=\"h-8 rounded-none border-r-0 px-2.5 lowercase data-size:h-8 [&_svg]:hidden\"\n          >\n            <SelectValue placeholder={filter.operator} />\n          </SelectTrigger>\n          <SelectContent id={operatorListboxId}>\n            <SelectGroup>\n              {filterOperators.map((operator) => (\n                <SelectItem\n                  key={operator.value}\n                  className=\"lowercase\"\n                  value={operator.value}\n                >\n                  {operator.label}\n                </SelectItem>\n              ))}\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n        {onFilterInputRender({\n          filter,\n          column,\n          inputId,\n          onFilterUpdate,\n          showValueSelector,\n          setShowValueSelector,\n        })}\n        <Button\n          aria-controls={filterItemId}\n          variant=\"ghost\"\n          className=\"h-full rounded-none rounded-r-md border border-input border-l-0 px-1.5 font-normal dark:bg-input/30\"\n          onClick={() => onFilterRemove(filter.filterId)}\n        >\n          <X className=\"size-3.5\" />\n        </Button>\n      </div>\n    );\n  }\n}\n\ninterface FilterValueSelectorProps<TData> {\n  column: Column<TData>;\n  value: string;\n  onSelect: (value: string) => void;\n}\n\nfunction FilterValueSelector<TData>({\n  column,\n  value,\n  onSelect,\n}: FilterValueSelectorProps<TData>) {\n  const variant = column.columnDef.meta?.variant ?? \"text\";\n\n  switch (variant) {\n    case \"boolean\":\n      return (\n        <CommandGroup>\n          <CommandItem value=\"true\" onSelect={() => onSelect(\"true\")}>\n            True\n          </CommandItem>\n          <CommandItem value=\"false\" onSelect={() => onSelect(\"false\")}>\n            False\n          </CommandItem>\n        </CommandGroup>\n      );\n\n    case \"select\":\n    case \"multiSelect\":\n      return (\n        <CommandGroup>\n          {column.columnDef.meta?.options?.map((option) => (\n            <CommandItem\n              key={option.value}\n              value={option.value}\n              className=\"[&>svg:last-child]:hidden\"\n              onSelect={() => onSelect(option.value)}\n            >\n              {option.icon && <option.icon />}\n              <span className=\"truncate\">{option.label}</span>\n              {option.count && (\n                <span className=\"ml-auto font-mono text-xs\">\n                  {option.count}\n                </span>\n              )}\n            </CommandItem>\n          ))}\n        </CommandGroup>\n      );\n\n    case \"date\":\n    case \"dateRange\":\n      return (\n        <Calendar\n          autoFocus\n          captionLayout=\"dropdown\"\n          mode=\"single\"\n          selected={value ? new Date(value) : undefined}\n          onSelect={(date) => onSelect(date?.getTime().toString() ?? \"\")}\n        />\n      );\n\n    default: {\n      const isEmpty = !value.trim();\n\n      return (\n        <CommandGroup>\n          <CommandItem\n            value={value}\n            onSelect={() => onSelect(value)}\n            disabled={isEmpty}\n          >\n            {isEmpty ? (\n              <>\n                <Text />\n                <span>Type to add filter...</span>\n              </>\n            ) : (\n              <>\n                <BadgeCheck />\n                <span className=\"truncate\">Filter by &quot;{value}&quot;</span>\n              </>\n            )}\n          </CommandItem>\n        </CommandGroup>\n      );\n    }\n  }\n}\n\nfunction onFilterInputRender<TData>({\n  filter,\n  column,\n  inputId,\n  onFilterUpdate,\n  showValueSelector,\n  setShowValueSelector,\n}: {\n  filter: ExtendedColumnFilter<TData>;\n  column: Column<TData>;\n  inputId: string;\n  onFilterUpdate: (\n    filterId: string,\n    updates: Partial<Omit<ExtendedColumnFilter<TData>, \"filterId\">>,\n  ) => void;\n  showValueSelector: boolean;\n  setShowValueSelector: (value: boolean) => void;\n}) {\n  if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\n    return (\n      <div\n        id={inputId}\n        role=\"status\"\n        aria-label={`${column.columnDef.meta?.label} filter is ${\n          filter.operator === \"isEmpty\" ? \"empty\" : \"not empty\"\n        }`}\n        aria-live=\"polite\"\n        className=\"h-full w-16 rounded-none border border-input bg-transparent px-1.5 py-0.5 text-muted-foreground dark:bg-input/30\"\n      />\n    );\n  }\n\n  switch (filter.variant) {\n    case \"text\":\n    case \"number\":\n    case \"range\": {\n      if (\n        (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\n        filter.operator === \"isBetween\"\n      ) {\n        return (\n          <DataTableRangeFilter\n            filter={filter}\n            column={column}\n            inputId={inputId}\n            onFilterUpdate={onFilterUpdate}\n            className=\"size-full max-w-28 gap-0 **:data-[slot='range-min']:border-r-0 [&_input]:rounded-none [&_input]:px-1.5\"\n          />\n        );\n      }\n\n      const isNumber =\n        filter.variant === \"number\" || filter.variant === \"range\";\n\n      return (\n        <Input\n          id={inputId}\n          type={isNumber ? \"number\" : \"text\"}\n          inputMode={isNumber ? \"numeric\" : undefined}\n          placeholder={column.columnDef.meta?.placeholder ?? \"Enter value...\"}\n          className=\"h-full w-24 rounded-none px-1.5\"\n          defaultValue={typeof filter.value === \"string\" ? filter.value : \"\"}\n          onChange={(event) =>\n            onFilterUpdate(filter.filterId, { value: event.target.value })\n          }\n        />\n      );\n    }\n\n    case \"boolean\": {\n      const inputListboxId = `${inputId}-listbox`;\n\n      return (\n        <Select\n          open={showValueSelector}\n          onOpenChange={setShowValueSelector}\n          value={typeof filter.value === \"string\" ? filter.value : \"true\"}\n          onValueChange={(value: \"true\" | \"false\") =>\n            onFilterUpdate(filter.filterId, { value })\n          }\n        >\n          <SelectTrigger\n            id={inputId}\n            aria-controls={inputListboxId}\n            className=\"rounded-none bg-transparent px-1.5 py-0.5 [&_svg]:hidden\"\n          >\n            <SelectValue placeholder={filter.value ? \"True\" : \"False\"} />\n          </SelectTrigger>\n          <SelectContent id={inputListboxId}>\n            <SelectGroup>\n              <SelectItem value=\"true\">True</SelectItem>\n              <SelectItem value=\"false\">False</SelectItem>\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n      );\n    }\n\n    case \"select\":\n    case \"multiSelect\": {\n      const inputListboxId = `${inputId}-listbox`;\n\n      const options = column.columnDef.meta?.options ?? [];\n      const selectedValues = Array.isArray(filter.value)\n        ? filter.value\n        : [filter.value];\n\n      const selectedOptions = options.filter((option) =>\n        selectedValues.includes(option.value),\n      );\n\n      return (\n        <Popover open={showValueSelector} onOpenChange={setShowValueSelector}>\n          <PopoverTrigger asChild>\n            <Button\n              id={inputId}\n              aria-controls={inputListboxId}\n              variant=\"ghost\"\n              className=\"h-full min-w-16 rounded-none border border-input px-1.5 font-normal dark:bg-input/30\"\n            >\n              {selectedOptions.length === 0 ? (\n                filter.variant === \"multiSelect\" ? (\n                  \"Select options...\"\n                ) : (\n                  \"Select option...\"\n                )\n              ) : (\n                <>\n                  <div className=\"flex items-center -space-x-2 rtl:space-x-reverse\">\n                    {selectedOptions.map((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                      ) : null,\n                    )}\n                  </div>\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            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                  {options.map((option) => {\n                    const isMultiSelect = filter.variant === \"multiSelect\";\n                    const isSelected = selectedValues.includes(option.value);\n\n                    return (\n                      <CommandItem\n                        key={option.value}\n                        value={option.value}\n                        data-checked={isMultiSelect ? isSelected : undefined}\n                        onSelect={() => {\n                          const value = isMultiSelect\n                            ? isSelected\n                              ? selectedValues.filter((v) => v !== option.value)\n                              : [...selectedValues, option.value]\n                            : option.value;\n                          onFilterUpdate(filter.filterId, { value });\n                        }}\n                      >\n                        {option.icon && <option.icon />}\n                        <span className=\"truncate\">{option.label}</span>\n                      </CommandItem>\n                    );\n                  })}\n                </CommandGroup>\n              </CommandList>\n            </Command>\n          </PopoverContent>\n        </Popover>\n      );\n    }\n\n    case \"date\":\n    case \"dateRange\": {\n      const inputListboxId = `${inputId}-listbox`;\n\n      const dateValue = Array.isArray(filter.value)\n        ? filter.value.filter(Boolean)\n        : [filter.value, filter.value].filter(Boolean);\n\n      const startDate = dateValue[0]\n        ? new Date(Number(dateValue[0]))\n        : undefined;\n      const endDate = dateValue[1] ? new Date(Number(dateValue[1])) : undefined;\n\n      const isSameDate =\n        startDate &&\n        endDate &&\n        startDate.toDateString() === endDate.toDateString();\n\n      const displayValue =\n        filter.operator === \"isBetween\" && dateValue.length === 2 && !isSameDate\n          ? `${formatDate(startDate, { month: \"short\" })} - ${formatDate(endDate, { month: \"short\" })}`\n          : startDate\n            ? formatDate(startDate, { month: \"short\" })\n            : \"Pick date...\";\n\n      return (\n        <Popover open={showValueSelector} onOpenChange={setShowValueSelector}>\n          <PopoverTrigger asChild>\n            <Button\n              id={inputId}\n              aria-controls={inputListboxId}\n              variant=\"ghost\"\n              className={cn(\n                \"h-full rounded-none border px-1.5 font-normal dark:bg-input/30\",\n                !filter.value && \"text-muted-foreground\",\n              )}\n            >\n              <CalendarIcon className=\"size-3.5\" />\n              <span className=\"truncate\">{displayValue}</span>\n            </Button>\n          </PopoverTrigger>\n          <PopoverContent\n            id={inputListboxId}\n            align=\"start\"\n            className=\"w-auto p-0\"\n          >\n            {filter.operator === \"isBetween\" ? (\n              <Calendar\n                autoFocus\n                captionLayout=\"dropdown\"\n                mode=\"range\"\n                selected={\n                  dateValue.length === 2\n                    ? {\n                        from: new Date(Number(dateValue[0])),\n                        to: new Date(Number(dateValue[1])),\n                      }\n                    : {\n                        from: new Date(),\n                        to: new Date(),\n                      }\n                }\n                onSelect={(date) => {\n                  onFilterUpdate(filter.filterId, {\n                    value: date\n                      ? [\n                          (date.from?.getTime() ?? \"\").toString(),\n                          (date.to?.getTime() ?? \"\").toString(),\n                        ]\n                      : [],\n                  });\n                }}\n              />\n            ) : (\n              <Calendar\n                autoFocus\n                captionLayout=\"dropdown\"\n                mode=\"single\"\n                selected={\n                  dateValue[0] ? new Date(Number(dateValue[0])) : undefined\n                }\n                onSelect={(date) => {\n                  onFilterUpdate(filter.filterId, {\n                    value: (date?.getTime() ?? \"\").toString(),\n                  });\n                }}\n              />\n            )}\n          </PopoverContent>\n        </Popover>\n      );\n    }\n\n    default:\n      return null;\n  }\n}\n",
      "type": "registry:component",
      "target": "src/components/data-table/data-table-filter-menu.tsx"
    },
    {
      "path": "src/components/data-table/data-table-range-filter.tsx",
      "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport type { ExtendedColumnFilter } from \"@/types/data-table\";\n\ninterface DataTableRangeFilterProps<TData> extends React.ComponentProps<\"div\"> {\n  filter: ExtendedColumnFilter<TData>;\n  column: Column<TData>;\n  inputId: string;\n  onFilterUpdate: (\n    filterId: string,\n    updates: Partial<Omit<ExtendedColumnFilter<TData>, \"filterId\">>,\n  ) => void;\n}\n\nexport function DataTableRangeFilter<TData>({\n  filter,\n  column,\n  inputId,\n  onFilterUpdate,\n  className,\n  ...props\n}: DataTableRangeFilterProps<TData>) {\n  const meta = column.columnDef.meta;\n\n  const [min, max] = React.useMemo(() => {\n    const range = column.columnDef.meta?.range;\n    if (range) return range;\n\n    const values = column.getFacetedMinMaxValues();\n    if (!values) return [0, 100];\n\n    return [values[0], values[1]];\n  }, [column]);\n\n  const formatValue = React.useCallback(\n    (value: string | number | undefined) => {\n      if (value === undefined || value === \"\") return \"\";\n      const numValue = Number(value);\n      return Number.isNaN(numValue)\n        ? \"\"\n        : numValue.toLocaleString(undefined, {\n            maximumFractionDigits: 0,\n          });\n    },\n    [],\n  );\n\n  const value = React.useMemo(() => {\n    if (Array.isArray(filter.value)) return filter.value.map(formatValue);\n    return [formatValue(filter.value), \"\"];\n  }, [filter.value, formatValue]);\n\n  const onRangeValueChange = React.useCallback(\n    (value: string, isMin?: boolean) => {\n      const numValue = Number(value);\n      const currentValues = Array.isArray(filter.value)\n        ? filter.value\n        : [\"\", \"\"];\n      const otherValue = isMin\n        ? (currentValues[1] ?? \"\")\n        : (currentValues[0] ?? \"\");\n\n      if (\n        value === \"\" ||\n        (!Number.isNaN(numValue) &&\n          (isMin\n            ? numValue >= min && numValue <= (Number(otherValue) || max)\n            : numValue <= max && numValue >= (Number(otherValue) || min)))\n      ) {\n        onFilterUpdate(filter.filterId, {\n          value: isMin ? [value, otherValue] : [otherValue, value],\n        });\n      }\n    },\n    [filter.filterId, filter.value, min, max, onFilterUpdate],\n  );\n\n  return (\n    <div\n      data-slot=\"range\"\n      className={cn(\"flex w-full items-center gap-2\", className)}\n      {...props}\n    >\n      <Input\n        id={`${inputId}-min`}\n        type=\"number\"\n        aria-label={`${meta?.label} minimum value`}\n        aria-valuemin={min}\n        aria-valuemax={max}\n        data-slot=\"range-min\"\n        inputMode=\"numeric\"\n        placeholder={min.toString()}\n        min={min}\n        max={max}\n        className=\"h-8 w-full rounded\"\n        defaultValue={value[0]}\n        onChange={(event) => onRangeValueChange(event.target.value, true)}\n      />\n      <span className=\"sr-only shrink-0 text-muted-foreground\">to</span>\n      <Input\n        id={`${inputId}-max`}\n        type=\"number\"\n        aria-label={`${meta?.label} maximum value`}\n        aria-valuemin={min}\n        aria-valuemax={max}\n        data-slot=\"range-max\"\n        inputMode=\"numeric\"\n        placeholder={max.toString()}\n        min={min}\n        max={max}\n        className=\"h-8 w-full rounded\"\n        defaultValue={value[1]}\n        onChange={(event) => onRangeValueChange(event.target.value)}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "src/components/data-table/data-table-range-filter.tsx"
    },
    {
      "path": "src/components/data-table/data-table-advanced-toolbar.tsx",
      "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps<TData>\n  extends React.ComponentProps<\"div\"> {\n  table: Table<TData>;\n}\n\nexport function DataTableAdvancedToolbar<TData>({\n  table,\n  children,\n  className,\n  ...props\n}: DataTableAdvancedToolbarProps<TData>) {\n  return (\n    <div\n      role=\"toolbar\"\n      aria-orientation=\"horizontal\"\n      className={cn(\n        \"flex w-full items-start justify-between gap-2 p-1\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex flex-1 flex-wrap items-center gap-2\">{children}</div>\n      <div className=\"flex items-center gap-2\">\n        <DataTableViewOptions table={table} align=\"end\" />\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "src/components/data-table/data-table-advanced-toolbar.tsx"
    },
    {
      "path": "src/hooks/use-callback-ref.ts",
      "content": "import * as React from \"react\";\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx\n */\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nfunction useCallbackRef<T extends (...args: never[]) => unknown>(\n  callback: T | undefined,\n): T {\n  const callbackRef = React.useRef(callback);\n\n  React.useEffect(() => {\n    callbackRef.current = callback;\n  });\n\n  // https://github.com/facebook/react/issues/19240\n  return React.useMemo(\n    () => ((...args) => callbackRef.current?.(...args)) as T,\n    [],\n  );\n}\n\nexport { useCallbackRef };\n",
      "type": "registry:hook"
    },
    {
      "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/data-table.ts",
      "content": "import type { Column } from \"@tanstack/react-table\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport type {\n  ExtendedColumnFilter,\n  FilterOperator,\n  FilterVariant,\n} from \"@/types/data-table\";\n\nexport function getColumnPinningStyle<TData>({\n  column,\n  withBorder = false,\n}: {\n  column: Column<TData>;\n  withBorder?: boolean;\n}): React.CSSProperties {\n  const isPinned = column.getIsPinned();\n  const isLastLeftPinnedColumn =\n    isPinned === \"left\" && column.getIsLastColumn(\"left\");\n  const isFirstRightPinnedColumn =\n    isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n  return {\n    boxShadow: withBorder\n      ? isLastLeftPinnedColumn\n        ? \"-4px 0 4px -4px var(--border) inset\"\n        : isFirstRightPinnedColumn\n          ? \"4px 0 4px -4px var(--border) inset\"\n          : undefined\n      : undefined,\n    left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n    right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\n    opacity: isPinned ? 0.97 : 1,\n    position: isPinned ? \"sticky\" : \"relative\",\n    background: isPinned ? \"var(--background)\" : \"var(--background)\",\n    width: column.getSize(),\n    zIndex: isPinned ? 1 : undefined,\n  };\n}\n\nexport function getFilterOperators(filterVariant: FilterVariant) {\n  const operatorMap: Record<\n    FilterVariant,\n    { label: string; value: FilterOperator }[]\n  > = {\n    text: dataTableConfig.textOperators,\n    number: dataTableConfig.numericOperators,\n    range: dataTableConfig.numericOperators,\n    date: dataTableConfig.dateOperators,\n    dateRange: dataTableConfig.dateOperators,\n    boolean: dataTableConfig.booleanOperators,\n    select: dataTableConfig.selectOperators,\n    multiSelect: dataTableConfig.multiSelectOperators,\n  };\n\n  return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\n}\n\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\n  const operators = getFilterOperators(filterVariant);\n\n  return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\n}\n\nexport function getValidFilters<TData>(\n  filters: ExtendedColumnFilter<TData>[],\n): ExtendedColumnFilter<TData>[] {\n  return filters.filter(\n    (filter) =>\n      filter.operator === \"isEmpty\" ||\n      filter.operator === \"isNotEmpty\" ||\n      (Array.isArray(filter.value)\n        ? filter.value.length > 0\n        : filter.value !== \"\" &&\n          filter.value !== null &&\n          filter.value !== undefined),\n  );\n}\n",
      "type": "registry:lib"
    },
    {
      "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/lib/id.ts",
      "content": "const DEFAULT_ALPHABET =\n  \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n\ninterface GenerateIdOptions {\n  alphabet?: string;\n  length?: number;\n}\n\nexport function generateId(\n  prefixOrOptions?: string | GenerateIdOptions,\n  inputOptions: GenerateIdOptions = {},\n): string {\n  const options =\n    typeof prefixOrOptions === \"object\" ? prefixOrOptions : inputOptions;\n\n  const { alphabet = DEFAULT_ALPHABET, length = 12 } = options;\n\n  const chars = new Array(length);\n\n  for (let i = 0; i < length; i++) {\n    chars[i] = alphabet[(Math.random() * alphabet.length) | 0];\n  }\n\n  return chars.join(\"\");\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "src/lib/parsers.ts",
      "content": "import { createParser } from \"nuqs/server\";\nimport { z } from \"zod\";\n\nimport { dataTableConfig } from \"@/config/data-table\";\n\nimport type {\n  ExtendedColumnFilter,\n  ExtendedColumnSort,\n} from \"@/types/data-table\";\n\nconst sortingItemSchema = z.object({\n  id: z.string(),\n  desc: z.boolean(),\n});\n\nexport const getSortingStateParser = <TData>(\n  columnIds?: string[] | Set<string>,\n) => {\n  const validKeys = columnIds\n    ? columnIds instanceof Set\n      ? columnIds\n      : new Set(columnIds)\n    : null;\n\n  return createParser({\n    parse: (value) => {\n      try {\n        const parsed = JSON.parse(value);\n        const result = z.array(sortingItemSchema).safeParse(parsed);\n\n        if (!result.success) return null;\n\n        if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n          return null;\n        }\n\n        return result.data as ExtendedColumnSort<TData>[];\n      } catch {\n        return null;\n      }\n    },\n    serialize: (value) => JSON.stringify(value),\n    eq: (a, b) =>\n      a.length === b.length &&\n      a.every(\n        (item, index) =>\n          item.id === b[index]?.id && item.desc === b[index]?.desc,\n      ),\n  });\n};\n\nconst filterItemSchema = z.object({\n  id: z.string(),\n  value: z.union([z.string(), z.array(z.string())]),\n  variant: z.enum(dataTableConfig.filterVariants),\n  operator: z.enum(dataTableConfig.operators),\n  filterId: z.string(),\n});\n\nexport type FilterItemSchema = z.infer<typeof filterItemSchema>;\n\nexport const getFiltersStateParser = <TData>(\n  columnIds?: string[] | Set<string>,\n) => {\n  const validKeys = columnIds\n    ? columnIds instanceof Set\n      ? columnIds\n      : new Set(columnIds)\n    : null;\n\n  return createParser({\n    parse: (value) => {\n      try {\n        const parsed = JSON.parse(value);\n        const result = z.array(filterItemSchema).safeParse(parsed);\n\n        if (!result.success) return null;\n\n        if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n          return null;\n        }\n\n        return result.data as ExtendedColumnFilter<TData>[];\n      } catch {\n        return null;\n      }\n    },\n    serialize: (value) => JSON.stringify(value),\n    eq: (a, b) =>\n      a.length === b.length &&\n      a.every(\n        (filter, index) =>\n          filter.id === b[index]?.id &&\n          filter.value === b[index]?.value &&\n          filter.variant === b[index]?.variant &&\n          filter.operator === b[index]?.operator,\n      ),\n  });\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "src/config/data-table.ts",
      "content": "export type DataTableConfig = typeof dataTableConfig;\n\nexport const dataTableConfig = {\n  textOperators: [\n    { label: \"Contains\", value: \"iLike\" as const },\n    { label: \"Does not contain\", value: \"notILike\" as const },\n    { label: \"Is\", value: \"eq\" as const },\n    { label: \"Is not\", value: \"ne\" as const },\n    { label: \"Is empty\", value: \"isEmpty\" as const },\n    { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n  ],\n  numericOperators: [\n    { label: \"Is\", value: \"eq\" as const },\n    { label: \"Is not\", value: \"ne\" as const },\n    { label: \"Is less than\", value: \"lt\" as const },\n    { label: \"Is less than or equal to\", value: \"lte\" as const },\n    { label: \"Is greater than\", value: \"gt\" as const },\n    { label: \"Is greater than or equal to\", value: \"gte\" as const },\n    { label: \"Is between\", value: \"isBetween\" as const },\n    { label: \"Is empty\", value: \"isEmpty\" as const },\n    { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n  ],\n  dateOperators: [\n    { label: \"Is\", value: \"eq\" as const },\n    { label: \"Is not\", value: \"ne\" as const },\n    { label: \"Is before\", value: \"lt\" as const },\n    { label: \"Is after\", value: \"gt\" as const },\n    { label: \"Is on or before\", value: \"lte\" as const },\n    { label: \"Is on or after\", value: \"gte\" as const },\n    { label: \"Is between\", value: \"isBetween\" as const },\n    { label: \"Is relative to today\", value: \"isRelativeToToday\" as const },\n    { label: \"Is empty\", value: \"isEmpty\" as const },\n    { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n  ],\n  selectOperators: [\n    { label: \"Is\", value: \"eq\" as const },\n    { label: \"Is not\", value: \"ne\" as const },\n    { label: \"Is empty\", value: \"isEmpty\" as const },\n    { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n  ],\n  multiSelectOperators: [\n    { label: \"Has any of\", value: \"inArray\" as const },\n    { label: \"Has none of\", value: \"notInArray\" as const },\n    { label: \"Is empty\", value: \"isEmpty\" as const },\n    { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n  ],\n  booleanOperators: [\n    { label: \"Is\", value: \"eq\" as const },\n    { label: \"Is not\", value: \"ne\" as const },\n  ],\n  sortOrders: [\n    { label: \"Asc\", value: \"asc\" as const },\n    { label: \"Desc\", value: \"desc\" as const },\n  ],\n  filterVariants: [\n    \"text\",\n    \"number\",\n    \"range\",\n    \"date\",\n    \"dateRange\",\n    \"boolean\",\n    \"select\",\n    \"multiSelect\",\n  ] as const,\n  operators: [\n    \"iLike\",\n    \"notILike\",\n    \"eq\",\n    \"ne\",\n    \"inArray\",\n    \"notInArray\",\n    \"isEmpty\",\n    \"isNotEmpty\",\n    \"lt\",\n    \"lte\",\n    \"gt\",\n    \"gte\",\n    \"isBetween\",\n    \"isRelativeToToday\",\n  ] as const,\n  joinOperators: [\"and\", \"or\"] as const,\n};\n",
      "type": "registry:file",
      "target": "src/config/data-table.ts"
    },
    {
      "path": "src/types/data-table.ts",
      "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\nimport type { DataTableConfig } from \"@/config/data-table\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\n\ndeclare module \"@tanstack/react-table\" {\n  interface TableMeta<TData extends RowData> {\n    queryKeys?: QueryKeys;\n  }\n\n  interface ColumnMeta<TData extends RowData, TValue> {\n    label?: string;\n    placeholder?: string;\n    variant?: FilterVariant;\n    options?: Option[];\n    range?: [number, number];\n    unit?: string;\n    icon?: React.ComponentType<React.ComponentProps<\"svg\">>;\n  }\n}\n\nexport interface QueryKeys {\n  page: string;\n  perPage: string;\n  sort: string;\n  filters: string;\n  joinOperator: string;\n}\n\nexport interface Option {\n  label: string;\n  value: string;\n  count?: number;\n  icon?: React.ComponentType<React.ComponentProps<\"svg\">>;\n}\n\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\n\nexport interface ExtendedColumnSort<TData> extends Omit<ColumnSort, \"id\"> {\n  id: Extract<keyof TData, string>;\n}\n\nexport interface ExtendedColumnFilter<TData> extends FilterItemSchema {\n  id: Extract<keyof TData, string>;\n}\n\nexport interface DataTableRowAction<TData> {\n  row: Row<TData>;\n  variant: \"update\" | \"delete\";\n}\n",
      "type": "registry:file",
      "target": "src/types/data-table.ts"
    }
  ],
  "type": "registry:component"
}