{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-filter",
  "type": "registry:block",
  "title": "Data table filter",
  "author": "Kian Bazarjani <kian@bazza.dev>",
  "description": "A powerful data table filter component inspired by Linear.",
  "dependencies": [
    "date-fns"
  ],
  "registryDependencies": [
    "button",
    "calendar",
    "checkbox",
    "command",
    "dropdown-menu",
    "input",
    "label",
    "popover",
    "separator",
    "slider",
    "table",
    "tabs",
    "use-mobile"
  ],
  "files": [
    {
      "path": "registry/data-table-filter/index.tsx",
      "content": "export { useDataTableFilters } from './hooks/use-data-table-filters'\nexport { DataTableFilter } from './components/data-table-filter'\n",
      "type": "registry:file",
      "target": "components/data-table-filter/index.tsx"
    },
    {
      "path": "registry/data-table-filter/components/active-filters.tsx",
      "content": "import { Button } from '@/components/ui/button'\nimport { Separator } from '@/components/ui/separator'\nimport { X } from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\nimport type {\n  Column,\n  ColumnDataType,\n  DataTableFilterActions,\n  FilterModel,\n  FilterStrategy,\n  FiltersState,\n} from '../core/types'\nimport { getColumn } from '../lib/helpers'\nimport type { Locale } from '../lib/i18n'\nimport { FilterOperator } from './filter-operator'\nimport { FilterSubject } from './filter-subject'\nimport { FilterValue } from './filter-value'\n\ninterface ActiveFiltersProps<TData> {\n  columns: Column<TData>[]\n  filters: FiltersState\n  actions: DataTableFilterActions\n  strategy: FilterStrategy\n  locale?: Locale\n}\n\nexport function ActiveFilters<TData>({\n  columns,\n  filters,\n  actions,\n  strategy,\n  locale = 'en',\n}: ActiveFiltersProps<TData>) {\n  return (\n    <>\n      {filters.map((filter) => {\n        const id = filter.columnId\n\n        const column = getColumn(columns, id)\n\n        // Skip if no filter value\n        if (!filter.values) return null\n\n        return (\n          <ActiveFilter\n            key={`active-filter-${filter.columnId}`}\n            filter={filter}\n            column={column}\n            actions={actions}\n            strategy={strategy}\n            locale={locale}\n          />\n        )\n      })}\n    </>\n  )\n}\n\ninterface ActiveFilterProps<TData, TType extends ColumnDataType> {\n  filter: FilterModel<TType>\n  column: Column<TData, TType>\n  actions: DataTableFilterActions\n  strategy: FilterStrategy\n  locale?: Locale\n}\n\n// Generic render function for a filter with type-safe value\nexport function ActiveFilter<TData, TType extends ColumnDataType>({\n  filter,\n  column,\n  actions,\n  strategy,\n  locale = 'en',\n}: ActiveFilterProps<TData, TType>) {\n  return (\n    <div className=\"flex h-7 items-center rounded-2xl border border-border bg-background shadow-xs text-xs\">\n      <FilterSubject column={column} />\n      <Separator orientation=\"vertical\" />\n      <FilterOperator\n        filter={filter}\n        column={column}\n        actions={actions}\n        locale={locale}\n      />\n      <Separator orientation=\"vertical\" />\n      <FilterValue\n        filter={filter}\n        column={column}\n        actions={actions}\n        strategy={strategy}\n        locale={locale}\n      />\n      <Separator orientation=\"vertical\" />\n      <Button\n        variant=\"ghost\"\n        className=\"rounded-none rounded-r-2xl text-xs w-7 h-full\"\n        onClick={() => actions.removeFilter(filter.columnId)}\n      >\n        <X className=\"size-4 -translate-x-0.5\" />\n      </Button>\n    </div>\n  )\n}\n\nexport function ActiveFiltersMobileContainer({\n  children,\n}: { children: React.ReactNode }) {\n  const scrollContainerRef = useRef<HTMLDivElement>(null)\n  const [showLeftBlur, setShowLeftBlur] = useState(false)\n  const [showRightBlur, setShowRightBlur] = useState(true)\n\n  // Check if there's content to scroll and update blur states\n  const checkScroll = () => {\n    if (scrollContainerRef.current) {\n      const { scrollLeft, scrollWidth, clientWidth } =\n        scrollContainerRef.current\n\n      // Show left blur if scrolled to the right\n      setShowLeftBlur(scrollLeft > 0)\n\n      // Show right blur if there's more content to scroll to the right\n      // Add a small buffer (1px) to account for rounding errors\n      setShowRightBlur(scrollLeft + clientWidth < scrollWidth - 1)\n    }\n  }\n\n  // Log blur states for debugging\n  // useEffect(() => {\n  //   console.log('left:', showLeftBlur, '  right:', showRightBlur)\n  // }, [showLeftBlur, showRightBlur])\n\n  // Set up ResizeObserver to monitor container size\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  useEffect(() => {\n    if (scrollContainerRef.current) {\n      const resizeObserver = new ResizeObserver(() => {\n        checkScroll()\n      })\n      resizeObserver.observe(scrollContainerRef.current)\n      return () => {\n        resizeObserver.disconnect()\n      }\n    }\n  }, [])\n\n  // Update blur states when children change\n  // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>\n  useEffect(() => {\n    checkScroll()\n  }, [children])\n\n  return (\n    <div className=\"relative w-full overflow-x-hidden\">\n      {/* Left blur effect */}\n      {showLeftBlur && (\n        <div className=\"absolute left-0 top-0 bottom-0 w-16 z-10 pointer-events-none bg-gradient-to-r from-background to-transparent animate-in fade-in-0\" />\n      )}\n\n      {/* Scrollable container */}\n      <div\n        ref={scrollContainerRef}\n        className=\"flex gap-2 overflow-x-scroll no-scrollbar\"\n        onScroll={checkScroll}\n      >\n        {children}\n      </div>\n\n      {/* Right blur effect */}\n      {showRightBlur && (\n        <div className=\"absolute right-0 top-0 bottom-0 w-16 z-10 pointer-events-none bg-gradient-to-l from-background to-transparent animate-in fade-in-0 \" />\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/components/active-filters.tsx"
    },
    {
      "path": "registry/data-table-filter/components/data-table-filter.tsx",
      "content": "'use client'\n\nimport { useIsMobile } from '@/hooks/use-mobile'\nimport type {\n  Column,\n  DataTableFilterActions,\n  FilterStrategy,\n  FiltersState,\n} from '../core/types'\nimport type { Locale } from '../lib/i18n'\nimport { ActiveFilters, ActiveFiltersMobileContainer } from './active-filters'\nimport { FilterActions } from './filter-actions'\nimport { FilterSelector } from './filter-selector'\n\ninterface DataTableFilterProps<TData> {\n  columns: Column<TData>[]\n  filters: FiltersState\n  actions: DataTableFilterActions\n  strategy: FilterStrategy\n  locale?: Locale\n}\n\nexport function DataTableFilter<TData>({\n  columns,\n  filters,\n  actions,\n  strategy,\n  locale = 'en',\n}: DataTableFilterProps<TData>) {\n  const isMobile = useIsMobile()\n  if (isMobile) {\n    return (\n      <div className=\"flex w-full items-start justify-between gap-2\">\n        <div className=\"flex gap-1\">\n          <FilterSelector\n            columns={columns}\n            filters={filters}\n            actions={actions}\n            strategy={strategy}\n            locale={locale}\n          />\n          <FilterActions\n            hasFilters={filters.length > 0}\n            actions={actions}\n            locale={locale}\n          />\n        </div>\n        <ActiveFiltersMobileContainer>\n          <ActiveFilters\n            columns={columns}\n            filters={filters}\n            actions={actions}\n            strategy={strategy}\n            locale={locale}\n          />\n        </ActiveFiltersMobileContainer>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"flex w-full items-start justify-between gap-2\">\n      <div className=\"flex md:flex-wrap gap-2 w-full flex-1\">\n        <FilterSelector\n          columns={columns}\n          filters={filters}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n        <ActiveFilters\n          columns={columns}\n          filters={filters}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n      </div>\n      <FilterActions\n        hasFilters={filters.length > 0}\n        actions={actions}\n        locale={locale}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/components/data-table-filter.tsx"
    },
    {
      "path": "registry/data-table-filter/components/filter-actions.tsx",
      "content": "import { Button } from '@/components/ui/button'\nimport { cn } from '@/lib/utils'\nimport { FilterXIcon } from 'lucide-react'\nimport { memo } from 'react'\nimport type { DataTableFilterActions } from '../core/types'\nimport { type Locale, t } from '../lib/i18n'\n\ninterface FilterActionsProps {\n  hasFilters: boolean\n  actions?: DataTableFilterActions\n  locale?: Locale\n}\n\nexport const FilterActions = memo(__FilterActions)\nfunction __FilterActions({\n  hasFilters,\n  actions,\n  locale = 'en',\n}: FilterActionsProps) {\n  return (\n    <Button\n      className={cn('h-7 !px-2', !hasFilters && 'hidden')}\n      variant=\"destructive\"\n      onClick={actions?.removeAllFilters}\n    >\n      <FilterXIcon />\n      <span className=\"hidden md:block\">{t('clear', locale)}</span>\n    </Button>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/components/filter-actions.tsx"
    },
    {
      "path": "registry/data-table-filter/components/filter-operator.tsx",
      "content": "import { Button } from '@/components/ui/button'\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from '@/components/ui/command'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { useState } from 'react'\nimport {\n  dateFilterOperators,\n  filterTypeOperatorDetails,\n  multiOptionFilterOperators,\n  numberFilterOperators,\n  optionFilterOperators,\n  textFilterOperators,\n} from '../core/operators'\nimport type {\n  Column,\n  ColumnDataType,\n  DataTableFilterActions,\n  FilterModel,\n  FilterOperators,\n} from '../core/types'\nimport { type Locale, t } from '../lib/i18n'\n\ninterface FilterOperatorProps<TData, TType extends ColumnDataType> {\n  column: Column<TData, TType>\n  filter: FilterModel<TType>\n  actions: DataTableFilterActions\n  locale?: Locale\n}\n\n// Renders the filter operator display and menu for a given column filter\n// The filter operator display is the label and icon for the filter operator\n// The filter operator menu is the dropdown menu for the filter operator\nexport function FilterOperator<TData, TType extends ColumnDataType>({\n  column,\n  filter,\n  actions,\n  locale = 'en',\n}: FilterOperatorProps<TData, TType>) {\n  const [open, setOpen] = useState<boolean>(false)\n\n  const close = () => setOpen(false)\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          className=\"m-0 h-full w-fit whitespace-nowrap rounded-none p-0 px-2 text-xs\"\n        >\n          <FilterOperatorDisplay\n            filter={filter}\n            columnType={column.type}\n            locale={locale}\n          />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        className=\"w-fit p-0 origin-(--radix-popover-content-transform-origin)\"\n      >\n        <Command loop>\n          <CommandInput placeholder={t('search', locale)} />\n          <CommandEmpty>{t('noresults', locale)}</CommandEmpty>\n          <CommandList className=\"max-h-fit\">\n            <FilterOperatorController\n              filter={filter}\n              column={column}\n              actions={actions}\n              closeController={close}\n              locale={locale}\n            />\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\ninterface FilterOperatorDisplayProps<TType extends ColumnDataType> {\n  filter: FilterModel<TType>\n  columnType: TType\n  locale?: Locale\n}\n\nexport function FilterOperatorDisplay<TType extends ColumnDataType>({\n  filter,\n  columnType,\n  locale = 'en',\n}: FilterOperatorDisplayProps<TType>) {\n  const operator = filterTypeOperatorDetails[columnType][filter.operator]\n  const label = t(operator.key, locale)\n\n  return <span className=\"text-muted-foreground\">{label}</span>\n}\n\ninterface FilterOperatorControllerProps<TData, TType extends ColumnDataType> {\n  filter: FilterModel<TType>\n  column: Column<TData, TType>\n  actions: DataTableFilterActions\n  closeController: () => void\n  locale?: Locale\n}\n\n/*\n *\n * TODO: Reduce into a single component. Each data type does not need it's own controller.\n *\n */\nexport function FilterOperatorController<TData, TType extends ColumnDataType>({\n  filter,\n  column,\n  actions,\n  closeController,\n  locale = 'en',\n}: FilterOperatorControllerProps<TData, TType>) {\n  switch (column.type) {\n    case 'option':\n      return (\n        <FilterOperatorOptionController\n          filter={filter as FilterModel<'option'>}\n          column={column as Column<TData, 'option'>}\n          actions={actions}\n          closeController={closeController}\n          locale={locale}\n        />\n      )\n    case 'multiOption':\n      return (\n        <FilterOperatorMultiOptionController\n          filter={filter as FilterModel<'multiOption'>}\n          column={column as Column<TData, 'multiOption'>}\n          actions={actions}\n          closeController={closeController}\n          locale={locale}\n        />\n      )\n    case 'date':\n      return (\n        <FilterOperatorDateController\n          filter={filter as FilterModel<'date'>}\n          column={column as Column<TData, 'date'>}\n          actions={actions}\n          closeController={closeController}\n          locale={locale}\n        />\n      )\n    case 'text':\n      return (\n        <FilterOperatorTextController\n          filter={filter as FilterModel<'text'>}\n          column={column as Column<TData, 'text'>}\n          actions={actions}\n          closeController={closeController}\n          locale={locale}\n        />\n      )\n    case 'number':\n      return (\n        <FilterOperatorNumberController\n          filter={filter as FilterModel<'number'>}\n          column={column as Column<TData, 'number'>}\n          actions={actions}\n          closeController={closeController}\n          locale={locale}\n        />\n      )\n    default:\n      return null\n  }\n}\n\nfunction FilterOperatorOptionController<TData>({\n  filter,\n  column,\n  actions,\n  closeController,\n  locale = 'en',\n}: FilterOperatorControllerProps<TData, 'option'>) {\n  const filterDetails = optionFilterOperators[filter.operator]\n\n  const relatedFilters = Object.values(optionFilterOperators).filter(\n    (o) => o.target === filterDetails.target,\n  )\n\n  const changeOperator = (value: string) => {\n    actions?.setFilterOperator(column.id, value as FilterOperators['option'])\n    closeController()\n  }\n\n  return (\n    <CommandGroup heading={t('operators', locale)}>\n      {relatedFilters.map((r) => {\n        return (\n          <CommandItem onSelect={changeOperator} value={r.value} key={r.value}>\n            {t(r.key, locale)}\n          </CommandItem>\n        )\n      })}\n    </CommandGroup>\n  )\n}\n\nfunction FilterOperatorMultiOptionController<TData>({\n  filter,\n  column,\n  actions,\n  closeController,\n  locale = 'en',\n}: FilterOperatorControllerProps<TData, 'multiOption'>) {\n  const filterDetails = multiOptionFilterOperators[filter.operator]\n\n  const relatedFilters = Object.values(multiOptionFilterOperators).filter(\n    (o) => o.target === filterDetails.target,\n  )\n\n  const changeOperator = (value: string) => {\n    actions?.setFilterOperator(\n      column.id,\n      value as FilterOperators['multiOption'],\n    )\n    closeController()\n  }\n\n  return (\n    <CommandGroup heading={t('operators', locale)}>\n      {relatedFilters.map((r) => {\n        return (\n          <CommandItem onSelect={changeOperator} value={r.value} key={r.value}>\n            {t(r.key, locale)}\n          </CommandItem>\n        )\n      })}\n    </CommandGroup>\n  )\n}\n\nfunction FilterOperatorDateController<TData>({\n  filter,\n  column,\n  actions,\n  closeController,\n  locale = 'en',\n}: FilterOperatorControllerProps<TData, 'date'>) {\n  const filterDetails = dateFilterOperators[filter.operator]\n\n  const relatedFilters = Object.values(dateFilterOperators).filter(\n    (o) => o.target === filterDetails.target,\n  )\n\n  const changeOperator = (value: string) => {\n    actions?.setFilterOperator(column.id, value as FilterOperators['date'])\n    closeController()\n  }\n\n  return (\n    <CommandGroup>\n      {relatedFilters.map((r) => {\n        return (\n          <CommandItem onSelect={changeOperator} value={r.value} key={r.value}>\n            {t(r.key, locale)}\n          </CommandItem>\n        )\n      })}\n    </CommandGroup>\n  )\n}\n\nexport function FilterOperatorTextController<TData>({\n  filter,\n  column,\n  actions,\n  closeController,\n  locale = 'en',\n}: FilterOperatorControllerProps<TData, 'text'>) {\n  const filterDetails = textFilterOperators[filter.operator]\n\n  const relatedFilters = Object.values(textFilterOperators).filter(\n    (o) => o.target === filterDetails.target,\n  )\n\n  const changeOperator = (value: string) => {\n    actions?.setFilterOperator(column.id, value as FilterOperators['text'])\n    closeController()\n  }\n\n  return (\n    <CommandGroup heading={t('operators', locale)}>\n      {relatedFilters.map((r) => {\n        return (\n          <CommandItem onSelect={changeOperator} value={r.value} key={r.value}>\n            {t(r.key, locale)}\n          </CommandItem>\n        )\n      })}\n    </CommandGroup>\n  )\n}\n\nfunction FilterOperatorNumberController<TData>({\n  filter,\n  column,\n  actions,\n  closeController,\n  locale = 'en',\n}: FilterOperatorControllerProps<TData, 'number'>) {\n  const filterDetails = numberFilterOperators[filter.operator]\n\n  const relatedFilters = Object.values(numberFilterOperators).filter(\n    (o) => o.target === filterDetails.target,\n  )\n\n  const changeOperator = (value: string) => {\n    actions?.setFilterOperator(column.id, value as FilterOperators['number'])\n    closeController()\n  }\n\n  return (\n    <div>\n      <CommandGroup heading={t('operators', locale)}>\n        {relatedFilters.map((r) => (\n          <CommandItem\n            onSelect={() => changeOperator(r.value)}\n            value={r.value}\n            key={r.value}\n          >\n            {t(r.key, locale)}\n          </CommandItem>\n        ))}\n      </CommandGroup>\n    </div>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/components/filter-operator.tsx"
    },
    {
      "path": "registry/data-table-filter/components/filter-selector.tsx",
      "content": "import { Button } from '@/components/ui/button'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from '@/components/ui/command'\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { cn } from '@/lib/utils'\nimport { ArrowRightIcon, ChevronRightIcon, FilterIcon } from 'lucide-react'\nimport {\n  isValidElement,\n  memo,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport React from 'react'\nimport type {\n  Column,\n  ColumnDataType,\n  DataTableFilterActions,\n  FilterStrategy,\n  FiltersState,\n} from '../core/types'\nimport { isAnyOf } from '../lib/array'\nimport { getColumn } from '../lib/helpers'\nimport { type Locale, t } from '../lib/i18n'\nimport { FilterValueController } from './filter-value'\n\ninterface FilterSelectorProps<TData> {\n  filters: FiltersState\n  columns: Column<TData>[]\n  actions: DataTableFilterActions\n  strategy: FilterStrategy\n  locale?: Locale\n}\n\nexport const FilterSelector = memo(__FilterSelector) as typeof __FilterSelector\n\nfunction __FilterSelector<TData>({\n  filters,\n  columns,\n  actions,\n  strategy,\n  locale = 'en',\n}: FilterSelectorProps<TData>) {\n  const [open, setOpen] = useState(false)\n  const [value, setValue] = useState('')\n  const [property, setProperty] = useState<string | undefined>(undefined)\n  const inputRef = useRef<HTMLInputElement>(null)\n\n  const column = property ? getColumn(columns, property) : undefined\n  const filter = property\n    ? filters.find((f) => f.columnId === property)\n    : undefined\n\n  const hasFilters = filters.length > 0\n\n  useEffect(() => {\n    if (property && inputRef) {\n      inputRef.current?.focus()\n      setValue('')\n    }\n  }, [property])\n\n  useEffect(() => {\n    if (!open) setTimeout(() => setValue(''), 150)\n  }, [open])\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: need filters to be updated\n  const content = useMemo(\n    () =>\n      property && column ? (\n        <FilterValueController\n          filter={filter!}\n          column={column as Column<TData, ColumnDataType>}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n      ) : (\n        <Command\n          loop\n          filter={(value, search, keywords) => {\n            const extendValue = `${value} ${keywords?.join(' ')}`\n            return extendValue.toLowerCase().includes(search.toLowerCase())\n              ? 1\n              : 0\n          }}\n        >\n          <CommandInput\n            value={value}\n            onValueChange={setValue}\n            ref={inputRef}\n            placeholder={t('search', locale)}\n          />\n          <CommandEmpty>{t('noresults', locale)}</CommandEmpty>\n          <CommandList className=\"max-h-fit\">\n            <CommandGroup>\n              {columns.map((column) => (\n                <FilterableColumn\n                  key={column.id}\n                  column={column}\n                  setProperty={setProperty}\n                />\n              ))}\n              <QuickSearchFilters\n                search={value}\n                filters={filters}\n                columns={columns}\n                actions={actions}\n                strategy={strategy}\n                locale={locale}\n              />\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      ),\n    [property, column, filter, filters, columns, actions, value],\n  )\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={async (value) => {\n        setOpen(value)\n        if (!value) setTimeout(() => setProperty(undefined), 100)\n      }}\n    >\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          className={cn('h-7', hasFilters && 'w-fit !px-2')}\n        >\n          <FilterIcon className=\"size-4\" />\n          {!hasFilters && <span>{t('filter', locale)}</span>}\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        side=\"bottom\"\n        className=\"w-fit p-0 origin-(--radix-popover-content-transform-origin)\"\n      >\n        {content}\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport function FilterableColumn<TData, TType extends ColumnDataType, TVal>({\n  column,\n  setProperty,\n}: {\n  column: Column<TData, TType, TVal>\n  setProperty: (value: string) => void\n}) {\n  const itemRef = useRef<HTMLDivElement>(null)\n\n  const prefetch = useCallback(() => {\n    column.prefetchOptions()\n    column.prefetchValues()\n    column.prefetchFacetedUniqueValues()\n    column.prefetchFacetedMinMaxValues()\n  }, [column])\n\n  useEffect(() => {\n    const target = itemRef.current\n\n    if (!target) return\n\n    // Set up MutationObserver\n    const observer = new MutationObserver((mutations) => {\n      for (const mutation of mutations) {\n        if (mutation.type === 'attributes') {\n          const isSelected = target.getAttribute('data-selected') === 'true'\n          if (isSelected) prefetch()\n        }\n      }\n    })\n\n    // Set up observer\n    observer.observe(target, {\n      attributes: true,\n      attributeFilter: ['data-selected'],\n    })\n\n    // Cleanup on unmount\n    return () => observer.disconnect()\n  }, [prefetch])\n\n  return (\n    <CommandItem\n      ref={itemRef}\n      value={column.id}\n      keywords={[column.displayName]}\n      onSelect={() => setProperty(column.id)}\n      className=\"group\"\n      onMouseEnter={prefetch}\n    >\n      <div className=\"flex w-full items-center justify-between\">\n        <div className=\"inline-flex items-center gap-1.5\">\n          {<column.icon strokeWidth={2.25} className=\"size-4\" />}\n          <span>{column.displayName}</span>\n        </div>\n        <ArrowRightIcon className=\"size-4 opacity-0 group-aria-selected:opacity-100\" />\n      </div>\n    </CommandItem>\n  )\n}\n\ninterface QuickSearchFiltersProps<TData> {\n  search?: string\n  filters: FiltersState\n  columns: Column<TData>[]\n  actions: DataTableFilterActions\n  strategy: FilterStrategy\n  locale?: Locale\n}\n\nexport const QuickSearchFilters = memo(\n  __QuickSearchFilters,\n) as typeof __QuickSearchFilters\n\nfunction __QuickSearchFilters<TData>({\n  search,\n  filters,\n  columns,\n  actions,\n  strategy,\n  locale = 'en',\n}: QuickSearchFiltersProps<TData>) {\n  if (!search || search.trim().length < 2) return null\n\n  const cols = useMemo(\n    () =>\n      columns.filter((c) =>\n        isAnyOf<ColumnDataType>(c.type, ['option', 'multiOption']),\n      ),\n    [columns],\n  )\n\n  return (\n    <>\n      {cols.map((column) => {\n        const filter = filters.find((f) => f.columnId === column.id)\n        const options = column.getOptions()\n        const optionsCount = column.getFacetedUniqueValues()\n\n        function handleOptionSelect(value: string, check: boolean) {\n          if (check) actions.addFilterValue(column, [value])\n          else actions.removeFilterValue(column, [value])\n        }\n\n        return (\n          <React.Fragment key={column.id}>\n            {options.map((v) => {\n              const checked = Boolean(filter?.values.includes(v.value))\n              const count = optionsCount?.get(v.value) ?? 0\n\n              return (\n                <CommandItem\n                  key={v.value}\n                  value={v.value}\n                  keywords={[v.label, v.value]}\n                  onSelect={() => {\n                    handleOptionSelect(v.value, !checked)\n                  }}\n                  className=\"group\"\n                >\n                  <div className=\"flex items-center gap-1.5 group\">\n                    <Checkbox\n                      checked={checked}\n                      className=\"opacity-0 data-[state=checked]:opacity-100 group-data-[selected=true]:opacity-100 dark:border-ring mr-1\"\n                    />\n                    <div className=\"flex items-center w-4 justify-center\">\n                      {v.icon &&\n                        (isValidElement(v.icon) ? (\n                          v.icon\n                        ) : (\n                          <v.icon className=\"size-4 text-primary\" />\n                        ))}\n                    </div>\n                    <div className=\"flex items-center gap-0.5\">\n                      <span className=\"text-muted-foreground\">\n                        {column.displayName}\n                      </span>\n                      <ChevronRightIcon className=\"size-3.5 text-muted-foreground/75\" />\n                      <span>\n                        {v.label}\n                        <sup\n                          className={cn(\n                            !optionsCount && 'hidden',\n                            'ml-0.5 tabular-nums tracking-tight text-muted-foreground',\n                            count === 0 && 'slashed-zero',\n                          )}\n                        >\n                          {count < 100 ? count : '100+'}\n                        </sup>\n                      </span>\n                    </div>\n                  </div>\n                </CommandItem>\n              )\n            })}\n          </React.Fragment>\n        )\n      })}\n    </>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/components/filter-selector.tsx"
    },
    {
      "path": "registry/data-table-filter/components/filter-subject.tsx",
      "content": "import type { Column, ColumnDataType } from '../core/types'\n\ninterface FilterSubjectProps<TData, TType extends ColumnDataType> {\n  column: Column<TData, TType>\n}\n\nexport function FilterSubject<TData, TType extends ColumnDataType>({\n  column,\n}: FilterSubjectProps<TData, TType>) {\n  const hasIcon = !!column.icon\n  return (\n    <span className=\"flex select-none items-center gap-1 whitespace-nowrap px-2 font-medium\">\n      {hasIcon && <column.icon className=\"size-4 stroke-[2.25px]\" />}\n      <span>{column.displayName}</span>\n    </span>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/components/filter-subject.tsx"
    },
    {
      "path": "registry/data-table-filter/components/filter-value.tsx",
      "content": "import { Button } from '@/components/ui/button'\nimport { Calendar } from '@/components/ui/calendar'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from '@/components/ui/command'\nimport {\n  Popover,\n  PopoverAnchor,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover'\nimport { Slider } from '@/components/ui/slider'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'\nimport { cn } from '@/lib/utils'\nimport { isEqual } from 'date-fns'\nimport { format } from 'date-fns'\nimport { Ellipsis } from 'lucide-react'\nimport {\n  cloneElement,\n  isValidElement,\n  memo,\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from 'react'\nimport type { DateRange } from 'react-day-picker'\nimport { numberFilterOperators } from '../core/operators'\nimport type {\n  Column,\n  ColumnDataType,\n  ColumnOptionExtended,\n  DataTableFilterActions,\n  FilterModel,\n  FilterStrategy,\n} from '../core/types'\nimport { useDebounceCallback } from '../hooks/use-debounce-callback'\nimport { take } from '../lib/array'\nimport { createNumberRange } from '../lib/helpers'\nimport { type Locale, t } from '../lib/i18n'\nimport { DebouncedInput } from '../ui/debounced-input'\n\ninterface FilterValueProps<TData, TType extends ColumnDataType> {\n  filter: FilterModel<TType>\n  column: Column<TData, TType>\n  actions: DataTableFilterActions\n  strategy: FilterStrategy\n  locale?: Locale\n}\n\nexport const FilterValue = memo(__FilterValue) as typeof __FilterValue\n\nfunction __FilterValue<TData, TType extends ColumnDataType>({\n  filter,\n  column,\n  actions,\n  strategy,\n  locale,\n}: FilterValueProps<TData, TType>) {\n  return (\n    <Popover>\n      <PopoverAnchor className=\"h-full\" />\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"ghost\"\n          className=\"m-0 h-full w-fit whitespace-nowrap rounded-none p-0 px-2 text-xs\"\n        >\n          <FilterValueDisplay\n            filter={filter}\n            column={column}\n            actions={actions}\n            locale={locale}\n          />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        side=\"bottom\"\n        className=\"w-fit p-0 origin-(--radix-popover-content-transform-origin)\"\n      >\n        <FilterValueController\n          filter={filter}\n          column={column}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n      </PopoverContent>\n    </Popover>\n  )\n}\n\ninterface FilterValueDisplayProps<TData, TType extends ColumnDataType> {\n  filter: FilterModel<TType>\n  column: Column<TData, TType>\n  actions: DataTableFilterActions\n  locale?: Locale\n}\n\nexport function FilterValueDisplay<TData, TType extends ColumnDataType>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueDisplayProps<TData, TType>) {\n  switch (column.type) {\n    case 'option':\n      return (\n        <FilterValueOptionDisplay\n          filter={filter as FilterModel<'option'>}\n          column={column as Column<TData, 'option'>}\n          actions={actions}\n          locale={locale}\n        />\n      )\n    case 'multiOption':\n      return (\n        <FilterValueMultiOptionDisplay\n          filter={filter as FilterModel<'multiOption'>}\n          column={column as Column<TData, 'multiOption'>}\n          actions={actions}\n          locale={locale}\n        />\n      )\n    case 'date':\n      return (\n        <FilterValueDateDisplay\n          filter={filter as FilterModel<'date'>}\n          column={column as Column<TData, 'date'>}\n          actions={actions}\n          locale={locale}\n        />\n      )\n    case 'text':\n      return (\n        <FilterValueTextDisplay\n          filter={filter as FilterModel<'text'>}\n          column={column as Column<TData, 'text'>}\n          actions={actions}\n          locale={locale}\n        />\n      )\n    case 'number':\n      return (\n        <FilterValueNumberDisplay\n          filter={filter as FilterModel<'number'>}\n          column={column as Column<TData, 'number'>}\n          actions={actions}\n          locale={locale}\n        />\n      )\n    default:\n      return null\n  }\n}\n\nexport function FilterValueOptionDisplay<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueDisplayProps<TData, 'option'>) {\n  const options = useMemo(() => column.getOptions(), [column])\n  const selected = options.filter((o) => filter?.values.includes(o.value))\n\n  // We display the selected options based on how many are selected\n  //\n  // If there is only one option selected, we display its icon and label\n  //\n  // If there are multiple options selected, we display:\n  // 1) up to 3 icons of the selected options\n  // 2) the number of selected options\n  if (selected.length === 1) {\n    const { label, icon: Icon } = selected[0]\n    const hasIcon = !!Icon\n    return (\n      <span className=\"inline-flex items-center gap-1\">\n        {hasIcon &&\n          (isValidElement(Icon) ? (\n            Icon\n          ) : (\n            <Icon className=\"size-4 text-primary\" />\n          ))}\n        <span>{label}</span>\n      </span>\n    )\n  }\n  const name = column.displayName.toLowerCase()\n  // TODO: Better pluralization for different languages\n  const pluralName = name.endsWith('s') ? `${name}es` : `${name}s`\n\n  const hasOptionIcons = !options?.some((o) => !o.icon)\n\n  return (\n    <div className=\"inline-flex items-center gap-0.5\">\n      {hasOptionIcons &&\n        take(selected, 3).map(({ value, icon }) => {\n          const Icon = icon!\n          return isValidElement(Icon) ? (\n            Icon\n          ) : (\n            <Icon key={value} className=\"size-4\" />\n          )\n        })}\n      <span className={cn(hasOptionIcons && 'ml-1.5')}>\n        {selected.length} {pluralName}\n      </span>\n    </div>\n  )\n}\n\nexport function FilterValueMultiOptionDisplay<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueDisplayProps<TData, 'multiOption'>) {\n  const options = useMemo(() => column.getOptions(), [column])\n  const selected = options.filter((o) => filter.values.includes(o.value))\n\n  if (selected.length === 1) {\n    const { label, icon: Icon } = selected[0]\n    const hasIcon = !!Icon\n    return (\n      <span className=\"inline-flex items-center gap-1.5\">\n        {hasIcon &&\n          (isValidElement(Icon) ? (\n            Icon\n          ) : (\n            <Icon className=\"size-4 text-primary\" />\n          ))}\n\n        <span>{label}</span>\n      </span>\n    )\n  }\n\n  const name = column.displayName.toLowerCase()\n\n  const hasOptionIcons = !options?.some((o) => !o.icon)\n\n  return (\n    <div className=\"inline-flex items-center gap-1.5\">\n      {hasOptionIcons && (\n        <div key=\"icons\" className=\"inline-flex items-center gap-0.5\">\n          {take(selected, 3).map(({ value, icon }) => {\n            const Icon = icon!\n            return isValidElement(Icon) ? (\n              cloneElement(Icon, { key: value })\n            ) : (\n              <Icon key={value} className=\"size-4\" />\n            )\n          })}\n        </div>\n      )}\n      <span>\n        {selected.length} {name}\n      </span>\n    </div>\n  )\n}\n\nfunction formatDateRange(start: Date, end: Date) {\n  const sameMonth = start.getMonth() === end.getMonth()\n  const sameYear = start.getFullYear() === end.getFullYear()\n\n  if (sameMonth && sameYear) {\n    return `${format(start, 'MMM d')} - ${format(end, 'd, yyyy')}`\n  }\n\n  if (sameYear) {\n    return `${format(start, 'MMM d')} - ${format(end, 'MMM d, yyyy')}`\n  }\n\n  return `${format(start, 'MMM d, yyyy')} - ${format(end, 'MMM d, yyyy')}`\n}\n\nexport function FilterValueDateDisplay<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueDisplayProps<TData, 'date'>) {\n  if (!filter) return null\n  if (filter.values.length === 0) return <Ellipsis className=\"size-4\" />\n  if (filter.values.length === 1) {\n    const value = filter.values[0]\n\n    const formattedDateStr = format(value, 'MMM d, yyyy')\n\n    return <span>{formattedDateStr}</span>\n  }\n\n  const formattedRangeStr = formatDateRange(filter.values[0], filter.values[1])\n\n  return <span>{formattedRangeStr}</span>\n}\n\nexport function FilterValueTextDisplay<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueDisplayProps<TData, 'text'>) {\n  if (!filter) return null\n  if (filter.values.length === 0 || filter.values[0].trim() === '')\n    return <Ellipsis className=\"size-4\" />\n\n  const value = filter.values[0]\n\n  return <span>{value}</span>\n}\n\nexport function FilterValueNumberDisplay<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueDisplayProps<TData, 'number'>) {\n  if (!filter || !filter.values || filter.values.length === 0) return null\n\n  if (\n    filter.operator === 'is between' ||\n    filter.operator === 'is not between'\n  ) {\n    const minValue = filter.values[0]\n    const maxValue = filter.values[1]\n\n    return (\n      <span className=\"tabular-nums tracking-tight\">\n        {minValue} {t('and', locale)} {maxValue}\n      </span>\n    )\n  }\n\n  const value = filter.values[0]\n  return <span className=\"tabular-nums tracking-tight\">{value}</span>\n}\n\n/****** Property Filter Value Controller ******/\n\ninterface FilterValueControllerProps<TData, TType extends ColumnDataType> {\n  filter: FilterModel<TType>\n  column: Column<TData, TType>\n  actions: DataTableFilterActions\n  strategy: FilterStrategy\n  locale?: Locale\n}\n\nexport const FilterValueController = memo(\n  __FilterValueController,\n) as typeof __FilterValueController\n\nfunction __FilterValueController<TData, TType extends ColumnDataType>({\n  filter,\n  column,\n  actions,\n  strategy,\n  locale = 'en',\n}: FilterValueControllerProps<TData, TType>) {\n  switch (column.type) {\n    case 'option':\n      return (\n        <FilterValueOptionController\n          filter={filter as FilterModel<'option'>}\n          column={column as Column<TData, 'option'>}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n      )\n    case 'multiOption':\n      return (\n        <FilterValueMultiOptionController\n          filter={filter as FilterModel<'multiOption'>}\n          column={column as Column<TData, 'multiOption'>}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n      )\n    case 'date':\n      return (\n        <FilterValueDateController\n          filter={filter as FilterModel<'date'>}\n          column={column as Column<TData, 'date'>}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n      )\n    case 'text':\n      return (\n        <FilterValueTextController\n          filter={filter as FilterModel<'text'>}\n          column={column as Column<TData, 'text'>}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n      )\n    case 'number':\n      return (\n        <FilterValueNumberController\n          filter={filter as FilterModel<'number'>}\n          column={column as Column<TData, 'number'>}\n          actions={actions}\n          strategy={strategy}\n          locale={locale}\n        />\n      )\n    default:\n      return null\n  }\n}\n\ninterface OptionItemProps {\n  option: ColumnOptionExtended & { initialSelected: boolean }\n  onToggle: (value: string, checked: boolean) => void\n}\n\n// Memoized option item to prevent re-renders unless its own props change\nconst OptionItem = memo(function OptionItem({\n  option,\n  onToggle,\n}: OptionItemProps) {\n  const { value, label, icon: Icon, selected, count } = option\n  const handleSelect = useCallback(() => {\n    onToggle(value, !selected)\n  }, [onToggle, value, selected])\n\n  return (\n    <CommandItem\n      key={value}\n      onSelect={handleSelect}\n      className=\"group flex items-center justify-between gap-1.5\"\n    >\n      <div className=\"flex items-center gap-1.5\">\n        <Checkbox\n          checked={selected}\n          className=\"opacity-0 data-[state=checked]:opacity-100 group-data-[selected=true]:opacity-100 dark:border-ring mr-1\"\n        />\n        {Icon &&\n          (isValidElement(Icon) ? (\n            Icon\n          ) : (\n            <Icon className=\"size-4 text-primary\" />\n          ))}\n        <span>\n          {label}\n          <sup\n            className={cn(\n              count == null && 'hidden',\n              'ml-0.5 tabular-nums tracking-tight text-muted-foreground',\n              count === 0 && 'slashed-zero',\n            )}\n          >\n            {typeof count === 'number' ? (count < 100 ? count : '100+') : ''}\n          </sup>\n        </span>\n      </div>\n    </CommandItem>\n  )\n})\n\nexport function FilterValueOptionController<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueControllerProps<TData, 'option'>) {\n  // Compute initial options once per mount\n  const initialOptions = useMemo(() => {\n    const counts = column.getFacetedUniqueValues()\n    return column.getOptions().map((o) => ({\n      ...o,\n      selected: filter?.values.includes(o.value),\n      initialSelected: filter?.values.includes(o.value),\n      count: counts?.get(o.value) ?? 0,\n    }))\n  }, [])\n\n  const [options, setOptions] = useState(initialOptions)\n\n  // Update selected state when filter values change\n  useEffect(() => {\n    setOptions((prev) =>\n      prev.map((o) => ({ ...o, selected: filter?.values.includes(o.value) })),\n    )\n  }, [filter?.values])\n\n  const handleToggle = useCallback(\n    (value: string, checked: boolean) => {\n      if (checked) actions.addFilterValue(column, [value])\n      else actions.removeFilterValue(column, [value])\n    },\n    [actions, column],\n  )\n\n  // Derive groups based on `initialSelected` only\n  const { selectedOptions, unselectedOptions } = useMemo(() => {\n    const sel: typeof options = []\n    const unsel: typeof options = []\n    for (const o of options) {\n      if (o.initialSelected) sel.push(o)\n      else unsel.push(o)\n    }\n    return { selectedOptions: sel, unselectedOptions: unsel }\n  }, [options])\n\n  return (\n    <Command loop>\n      <CommandInput autoFocus placeholder={t('search', locale)} />\n      <CommandEmpty>{t('noresults', locale)}</CommandEmpty>\n      <CommandList className=\"max-h-fit\">\n        <CommandGroup className={cn(selectedOptions.length === 0 && 'hidden')}>\n          {selectedOptions.map((option) => (\n            <OptionItem\n              key={option.value}\n              option={option}\n              onToggle={handleToggle}\n            />\n          ))}\n        </CommandGroup>\n        <CommandSeparator />\n        <CommandGroup\n          className={cn(unselectedOptions.length === 0 && 'hidden')}\n        >\n          {unselectedOptions.map((option) => (\n            <OptionItem\n              key={option.value}\n              option={option}\n              onToggle={handleToggle}\n            />\n          ))}\n        </CommandGroup>\n      </CommandList>\n    </Command>\n  )\n}\n\nexport function FilterValueMultiOptionController<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueControllerProps<TData, 'multiOption'>) {\n  // Compute initial options once per mount\n  const initialOptions = useMemo(() => {\n    const counts = column.getFacetedUniqueValues()\n    return column.getOptions().map((o) => {\n      const selected = filter?.values.includes(o.value)\n      return {\n        ...o,\n        selected,\n        initialSelected: selected,\n        count: counts?.get(o.value) ?? 0,\n      }\n    })\n  }, [])\n\n  const [options, setOptions] = useState(initialOptions)\n\n  // Update selected state when filter values change\n  useEffect(() => {\n    setOptions((prev) =>\n      prev.map((o) => ({ ...o, selected: filter?.values.includes(o.value) })),\n    )\n  }, [filter?.values])\n\n  const handleToggle = useCallback(\n    (value: string, checked: boolean) => {\n      if (checked) actions.addFilterValue(column, [value])\n      else actions.removeFilterValue(column, [value])\n    },\n    [actions, column],\n  )\n\n  // Derive groups based on `initialSelected` only\n  const { selectedOptions, unselectedOptions } = useMemo(() => {\n    const sel: typeof options = []\n    const unsel: typeof options = []\n    for (const o of options) {\n      if (o.initialSelected) sel.push(o)\n      else unsel.push(o)\n    }\n    return { selectedOptions: sel, unselectedOptions: unsel }\n  }, [options])\n\n  return (\n    <Command loop>\n      <CommandInput autoFocus placeholder={t('search', locale)} />\n      <CommandEmpty>{t('noresults', locale)}</CommandEmpty>\n      <CommandList>\n        <CommandGroup className={cn(selectedOptions.length === 0 && 'hidden')}>\n          {selectedOptions.map((option) => (\n            <OptionItem\n              key={option.value}\n              option={option}\n              onToggle={handleToggle}\n            />\n          ))}\n        </CommandGroup>\n        <CommandSeparator />\n        <CommandGroup\n          className={cn(unselectedOptions.length === 0 && 'hidden')}\n        >\n          {unselectedOptions.map((option) => (\n            <OptionItem\n              key={option.value}\n              option={option}\n              onToggle={handleToggle}\n            />\n          ))}\n        </CommandGroup>\n      </CommandList>\n    </Command>\n  )\n}\n\nexport function FilterValueDateController<TData>({\n  filter,\n  column,\n  actions,\n}: FilterValueControllerProps<TData, 'date'>) {\n  const [date, setDate] = useState<DateRange | undefined>({\n    from: filter?.values[0] ?? new Date(),\n    to: filter?.values[1] ?? undefined,\n  })\n\n  function changeDateRange(value: DateRange | undefined) {\n    const start = value?.from\n    const end =\n      start && value && value.to && !isEqual(start, value.to)\n        ? value.to\n        : undefined\n\n    setDate({ from: start, to: end })\n\n    const isRange = start && end\n    const newValues = isRange ? [start, end] : start ? [start] : []\n\n    actions.setFilterValue(column, newValues)\n  }\n\n  return (\n    <Command>\n      <CommandList className=\"max-h-fit\">\n        <CommandGroup>\n          <div>\n            <Calendar\n              initialFocus\n              mode=\"range\"\n              defaultMonth={date?.from}\n              selected={date}\n              onSelect={changeDateRange}\n              numberOfMonths={1}\n            />\n          </div>\n        </CommandGroup>\n      </CommandList>\n    </Command>\n  )\n}\n\nexport function FilterValueTextController<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueControllerProps<TData, 'text'>) {\n  const changeText = (value: string | number) => {\n    actions.setFilterValue(column, [String(value)])\n  }\n\n  return (\n    <Command>\n      <CommandList className=\"max-h-fit\">\n        <CommandGroup>\n          <CommandItem>\n            <DebouncedInput\n              placeholder={t('search', locale)}\n              autoFocus\n              value={filter?.values[0] ?? ''}\n              onChange={changeText}\n            />\n          </CommandItem>\n        </CommandGroup>\n      </CommandList>\n    </Command>\n  )\n}\n\nexport function FilterValueNumberController<TData>({\n  filter,\n  column,\n  actions,\n  locale = 'en',\n}: FilterValueControllerProps<TData, 'number'>) {\n  const minMax = useMemo(() => column.getFacetedMinMaxValues(), [column])\n  const [sliderMin, sliderMax] = [\n    minMax ? minMax[0] : 0,\n    minMax ? minMax[1] : 0,\n  ]\n\n  // Local state for values\n  const [values, setValues] = useState(filter?.values ?? [0, 0])\n\n  // Sync with parent filter changes\n  useEffect(() => {\n    if (\n      filter?.values &&\n      filter.values.length === values.length &&\n      filter.values.every((v, i) => v === values[i])\n    ) {\n      setValues(filter.values)\n    }\n  }, [filter?.values, values])\n\n  const isNumberRange =\n    // filter && values.length === 2\n    filter && numberFilterOperators[filter.operator].target === 'multiple'\n\n  const setFilterOperatorDebounced = useDebounceCallback(\n    actions.setFilterOperator,\n    500,\n  )\n  const setFilterValueDebounced = useDebounceCallback(\n    actions.setFilterValue,\n    500,\n  )\n\n  const changeNumber = (value: number[]) => {\n    setValues(value)\n    setFilterValueDebounced(column as any, value)\n  }\n\n  const changeMinNumber = (value: number) => {\n    const newValues = createNumberRange([value, values[1]])\n    setValues(newValues)\n    setFilterValueDebounced(column as any, newValues)\n  }\n\n  const changeMaxNumber = (value: number) => {\n    const newValues = createNumberRange([values[0], value])\n    setValues(newValues)\n    setFilterValueDebounced(column as any, newValues)\n  }\n\n  const changeType = useCallback(\n    (type: 'single' | 'range') => {\n      let newValues: number[] = []\n      if (type === 'single')\n        newValues = [values[0]] // Keep the first value for single mode\n      else if (!minMax)\n        newValues = createNumberRange([values[0], values[1] ?? 0])\n      else {\n        const value = values[0]\n        newValues =\n          value - minMax[0] < minMax[1] - value\n            ? createNumberRange([value, minMax[1]])\n            : createNumberRange([minMax[0], value])\n      }\n\n      const newOperator = type === 'single' ? 'is' : 'is between'\n\n      // Update local state\n      setValues(newValues)\n\n      // Cancel in-flight debounced calls to prevent flicker/race conditions\n      setFilterOperatorDebounced.cancel()\n      setFilterValueDebounced.cancel()\n\n      // Update global filter state atomically\n      actions.setFilterOperator(column.id, newOperator)\n      actions.setFilterValue(column, newValues)\n    },\n    [values, column, actions, minMax],\n  )\n\n  return (\n    <Command>\n      <CommandList className=\"w-[300px] px-2 py-2\">\n        <CommandGroup>\n          <div className=\"flex flex-col w-full\">\n            <Tabs\n              value={isNumberRange ? 'range' : 'single'}\n              onValueChange={(v) => changeType(v as 'single' | 'range')}\n            >\n              <TabsList className=\"w-full *:text-xs\">\n                <TabsTrigger value=\"single\">{t('single', locale)}</TabsTrigger>\n                <TabsTrigger value=\"range\">{t('range', locale)}</TabsTrigger>\n              </TabsList>\n              <TabsContent value=\"single\" className=\"flex flex-col gap-4 mt-4\">\n                {minMax && (\n                  <Slider\n                    value={[values[0]]}\n                    onValueChange={(value) => changeNumber(value)}\n                    min={sliderMin}\n                    max={sliderMax}\n                    step={1}\n                    aria-orientation=\"horizontal\"\n                  />\n                )}\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"text-xs font-medium\">\n                    {t('value', locale)}\n                  </span>\n                  <DebouncedInput\n                    id=\"single\"\n                    type=\"number\"\n                    value={values[0].toString()} // Use values[0] directly\n                    onChange={(v) => changeNumber([Number(v)])}\n                  />\n                </div>\n              </TabsContent>\n              <TabsContent value=\"range\" className=\"flex flex-col gap-4 mt-4\">\n                {minMax && (\n                  <Slider\n                    value={values} // Use values directly\n                    onValueChange={changeNumber}\n                    min={sliderMin}\n                    max={sliderMax}\n                    step={1}\n                    aria-orientation=\"horizontal\"\n                  />\n                )}\n                <div className=\"grid grid-cols-2 gap-4\">\n                  <div className=\"flex items-center gap-2\">\n                    <span className=\"text-xs font-medium\">\n                      {t('min', locale)}\n                    </span>\n                    <DebouncedInput\n                      type=\"number\"\n                      value={values[0]}\n                      onChange={(v) => changeMinNumber(Number(v))}\n                    />\n                  </div>\n                  <div className=\"flex items-center gap-2\">\n                    <span className=\"text-xs font-medium\">\n                      {t('max', locale)}\n                    </span>\n                    <DebouncedInput\n                      type=\"number\"\n                      value={values[1]}\n                      onChange={(v) => changeMaxNumber(Number(v))}\n                    />\n                  </div>\n                </div>\n              </TabsContent>\n            </Tabs>\n          </div>\n        </CommandGroup>\n      </CommandList>\n    </Command>\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/components/filter-value.tsx"
    },
    {
      "path": "registry/data-table-filter/core/filters.ts",
      "content": "import { isAnyOf, uniq } from '../lib/array'\nimport { isColumnOptionArray } from '../lib/helpers'\nimport { memo } from '../lib/memo'\nimport type {\n  Column,\n  ColumnConfig,\n  ColumnDataType,\n  ColumnOption,\n  ElementType,\n  FilterStrategy,\n  Nullable,\n  TAccessorFn,\n  TOrderFn,\n  TTransformOptionFn,\n} from './types'\n\nclass ColumnConfigBuilder<\n  TData,\n  TType extends ColumnDataType = any,\n  TVal = unknown,\n  TId extends string = string, // Add TId generic\n> {\n  private config: Partial<ColumnConfig<TData, TType, TVal, TId>>\n\n  constructor(type: TType) {\n    this.config = { type } as Partial<ColumnConfig<TData, TType, TVal, TId>>\n  }\n\n  private clone(): ColumnConfigBuilder<TData, TType, TVal, TId> {\n    const newInstance = new ColumnConfigBuilder<TData, TType, TVal, TId>(\n      this.config.type as TType,\n    )\n    newInstance.config = { ...this.config }\n    return newInstance\n  }\n\n  id<TNewId extends string>(\n    value: TNewId,\n  ): ColumnConfigBuilder<TData, TType, TVal, TNewId> {\n    const newInstance = this.clone() as any // We'll refine this\n    newInstance.config.id = value\n    return newInstance as ColumnConfigBuilder<TData, TType, TVal, TNewId>\n  }\n\n  accessor<TNewVal>(\n    accessor: TAccessorFn<TData, TNewVal>,\n  ): ColumnConfigBuilder<TData, TType, TNewVal, TId> {\n    const newInstance = this.clone() as any\n    newInstance.config.accessor = accessor\n    return newInstance as ColumnConfigBuilder<TData, TType, TNewVal, TId>\n  }\n\n  displayName(value: string): ColumnConfigBuilder<TData, TType, TVal, TId> {\n    const newInstance = this.clone()\n    newInstance.config.displayName = value\n    return newInstance\n  }\n\n  icon(value: any): ColumnConfigBuilder<TData, TType, TVal, TId> {\n    const newInstance = this.clone()\n    newInstance.config.icon = value\n    return newInstance\n  }\n\n  min(\n    value: number,\n  ): ColumnConfigBuilder<\n    TData,\n    TType extends 'number' ? TType : never,\n    TVal,\n    TId\n  > {\n    if (this.config.type !== 'number') {\n      throw new Error('min() is only applicable to number columns')\n    }\n    const newInstance = this.clone() as any\n    newInstance.config.min = value\n    return newInstance\n  }\n\n  max(\n    value: number,\n  ): ColumnConfigBuilder<\n    TData,\n    TType extends 'number' ? TType : never,\n    TVal,\n    TId\n  > {\n    if (this.config.type !== 'number') {\n      throw new Error('max() is only applicable to number columns')\n    }\n    const newInstance = this.clone() as any\n    newInstance.config.max = value\n    return newInstance\n  }\n\n  options(\n    value: ColumnOption[],\n  ): ColumnConfigBuilder<\n    TData,\n    TType extends 'option' | 'multiOption' ? TType : never,\n    TVal,\n    TId\n  > {\n    if (!isAnyOf(this.config.type, ['option', 'multiOption'])) {\n      throw new Error(\n        'options() is only applicable to option or multiOption columns',\n      )\n    }\n    const newInstance = this.clone() as any\n    newInstance.config.options = value\n    return newInstance\n  }\n\n  transformOptionFn(\n    fn: TTransformOptionFn<TVal>,\n  ): ColumnConfigBuilder<\n    TData,\n    TType extends 'option' | 'multiOption' ? TType : never,\n    TVal,\n    TId\n  > {\n    if (!isAnyOf(this.config.type, ['option', 'multiOption'])) {\n      throw new Error(\n        'transformOptionFn() is only applicable to option or multiOption columns',\n      )\n    }\n    const newInstance = this.clone() as any\n    newInstance.config.transformOptionFn = fn\n    return newInstance\n  }\n\n  orderFn(\n    fn: TOrderFn<TVal>,\n  ): ColumnConfigBuilder<\n    TData,\n    TType extends 'option' | 'multiOption' ? TType : never,\n    TVal,\n    TId\n  > {\n    if (!isAnyOf(this.config.type, ['option', 'multiOption'])) {\n      throw new Error(\n        'orderFn() is only applicable to option or multiOption columns',\n      )\n    }\n    const newInstance = this.clone() as any\n    newInstance.config.orderFn = fn\n    return newInstance\n  }\n\n  build(): ColumnConfig<TData, TType, TVal, TId> {\n    if (!this.config.id) throw new Error('id is required')\n    if (!this.config.accessor) throw new Error('accessor is required')\n    if (!this.config.displayName) throw new Error('displayName is required')\n    if (!this.config.icon) throw new Error('icon is required')\n    return this.config as ColumnConfig<TData, TType, TVal, TId>\n  }\n}\n\n// Update the helper interface\ninterface FluentColumnConfigHelper<TData> {\n  text: () => ColumnConfigBuilder<TData, 'text', string>\n  number: () => ColumnConfigBuilder<TData, 'number', number>\n  date: () => ColumnConfigBuilder<TData, 'date', Date>\n  option: () => ColumnConfigBuilder<TData, 'option', string>\n  multiOption: () => ColumnConfigBuilder<TData, 'multiOption', string[]>\n}\n\n// Factory function remains mostly the same\nexport function createColumnConfigHelper<\n  TData,\n>(): FluentColumnConfigHelper<TData> {\n  return {\n    text: () => new ColumnConfigBuilder<TData, 'text', string>('text'),\n    number: () => new ColumnConfigBuilder<TData, 'number', number>('number'),\n    date: () => new ColumnConfigBuilder<TData, 'date', Date>('date'),\n    option: () => new ColumnConfigBuilder<TData, 'option', string>('option'),\n    multiOption: () =>\n      new ColumnConfigBuilder<TData, 'multiOption', string[]>('multiOption'),\n  }\n}\n\nexport function getColumnOptions<TData, TType extends ColumnDataType, TVal>(\n  column: ColumnConfig<TData, TType, TVal>,\n  data: TData[],\n  strategy: FilterStrategy,\n): ColumnOption[] {\n  if (!isAnyOf(column.type, ['option', 'multiOption'])) {\n    console.warn(\n      'Column options can only be retrieved for option and multiOption columns',\n    )\n    return []\n  }\n\n  if (strategy === 'server' && !column.options) {\n    throw new Error('column options are required for server-side filtering')\n  }\n\n  if (column.options) {\n    return column.options\n  }\n\n  const filtered = data\n    .flatMap(column.accessor)\n    .filter((v): v is NonNullable<TVal> => v !== undefined && v !== null)\n\n  let models = uniq(filtered)\n\n  if (column.orderFn) {\n    models = models.sort((m1, m2) =>\n      column.orderFn!(\n        m1 as ElementType<NonNullable<TVal>>,\n        m2 as ElementType<NonNullable<TVal>>,\n      ),\n    )\n  }\n\n  if (column.transformOptionFn) {\n    // Memoize transformOptionFn calls\n    const memoizedTransform = memo(\n      () => [models],\n      (deps) =>\n        deps[0].map((m) =>\n          column.transformOptionFn!(m as ElementType<NonNullable<TVal>>),\n        ),\n      { key: `transform-${column.id}` },\n    )\n    return memoizedTransform()\n  }\n\n  if (isColumnOptionArray(models)) return models\n\n  throw new Error(\n    `[data-table-filter] [${column.id}] Either provide static options, a transformOptionFn, or ensure the column data conforms to ColumnOption type`,\n  )\n}\n\nexport function getColumnValues<TData, TType extends ColumnDataType, TVal>(\n  column: ColumnConfig<TData, TType, TVal>,\n  data: TData[],\n) {\n  // Memoize accessor calls\n  const memoizedAccessor = memo(\n    () => [data],\n    (deps) =>\n      deps[0]\n        .flatMap(column.accessor)\n        .filter(\n          (v): v is NonNullable<TVal> => v !== undefined && v !== null,\n        ) as ElementType<NonNullable<TVal>>[],\n    { key: `accessor-${column.id}` },\n  )\n\n  const raw = memoizedAccessor()\n\n  if (!isAnyOf(column.type, ['option', 'multiOption'])) {\n    return raw\n  }\n\n  if (column.options) {\n    return raw\n      .map((v) => column.options?.find((o) => o.value === v)?.value)\n      .filter((v) => v !== undefined && v !== null)\n  }\n\n  if (column.transformOptionFn) {\n    const memoizedTransform = memo(\n      () => [raw],\n      (deps) =>\n        deps[0].map(\n          (v) => column.transformOptionFn!(v) as ElementType<NonNullable<TVal>>,\n        ),\n      { key: `transform-values-${column.id}` },\n    )\n    return memoizedTransform()\n  }\n\n  if (isColumnOptionArray(raw)) {\n    return raw\n  }\n\n  throw new Error(\n    `[data-table-filter] [${column.id}] Either provide static options, a transformOptionFn, or ensure the column data conforms to ColumnOption type`,\n  )\n}\n\nexport function getFacetedUniqueValues<\n  TData,\n  TType extends ColumnDataType,\n  TVal,\n>(\n  column: ColumnConfig<TData, TType, TVal>,\n  values: string[] | ColumnOption[],\n  strategy: FilterStrategy,\n): Map<string, number> | undefined {\n  if (!isAnyOf(column.type, ['option', 'multiOption'])) {\n    console.warn(\n      'Faceted unique values can only be retrieved for option and multiOption columns',\n    )\n    return new Map<string, number>()\n  }\n\n  if (strategy === 'server') {\n    return column.facetedOptions\n  }\n\n  const acc = new Map<string, number>()\n\n  if (isColumnOptionArray(values)) {\n    for (const option of values) {\n      const curr = acc.get(option.value) ?? 0\n      acc.set(option.value, curr + 1)\n    }\n  } else {\n    for (const option of values) {\n      const curr = acc.get(option as string) ?? 0\n      acc.set(option as string, curr + 1)\n    }\n  }\n\n  return acc\n}\n\nexport function getFacetedMinMaxValues<\n  TData,\n  TType extends ColumnDataType,\n  TVal,\n>(\n  column: ColumnConfig<TData, TType, TVal>,\n  data: TData[],\n  strategy: FilterStrategy,\n): [number, number] | undefined {\n  if (column.type !== 'number') return undefined // Only applicable to number columns\n\n  if (typeof column.min === 'number' && typeof column.max === 'number') {\n    return [column.min, column.max]\n  }\n\n  if (strategy === 'server') {\n    return undefined\n  }\n\n  const values = data\n    .flatMap((row) => column.accessor(row) as Nullable<number>)\n    .filter((v): v is number => typeof v === 'number' && !Number.isNaN(v))\n\n  if (values.length === 0) {\n    return [0, 0] // Fallback to config or reasonable defaults\n  }\n\n  const min = Math.min(...values)\n  const max = Math.max(...values)\n\n  return [min, max]\n}\n\nexport function createColumns<TData>(\n  data: TData[],\n  columnConfigs: ReadonlyArray<ColumnConfig<TData, any, any, any>>,\n  strategy: FilterStrategy,\n): Column<TData>[] {\n  return columnConfigs.map((columnConfig) => {\n    const getOptions: () => ColumnOption[] = memo(\n      () => [data, strategy, columnConfig.options],\n      ([data, strategy]) =>\n        getColumnOptions(columnConfig, data as any, strategy as any),\n      { key: `options-${columnConfig.id}` },\n    )\n\n    const getValues: () => ElementType<NonNullable<any>>[] = memo(\n      () => [data, strategy],\n      () => (strategy === 'client' ? getColumnValues(columnConfig, data) : []),\n      { key: `values-${columnConfig.id}` },\n    )\n\n    const getUniqueValues: () => Map<string, number> | undefined = memo(\n      () => [getValues(), strategy],\n      ([values, strategy]) =>\n        getFacetedUniqueValues(columnConfig, values as any, strategy as any),\n      { key: `faceted-${columnConfig.id}` },\n    )\n\n    const getMinMaxValues: () => [number, number] | undefined = memo(\n      () => [data, strategy],\n      () => getFacetedMinMaxValues(columnConfig, data, strategy),\n      { key: `minmax-${columnConfig.id}` },\n    )\n\n    // Create the Column instance\n    const column: Column<TData> = {\n      ...columnConfig,\n      getOptions,\n      getValues,\n      getFacetedUniqueValues: getUniqueValues,\n      getFacetedMinMaxValues: getMinMaxValues,\n      // Prefetch methods will be added below\n      prefetchOptions: async () => {}, // Placeholder, defined below\n      prefetchValues: async () => {},\n      prefetchFacetedUniqueValues: async () => {},\n      prefetchFacetedMinMaxValues: async () => {},\n      _prefetchedOptionsCache: null, // Initialize private cache\n      _prefetchedValuesCache: null,\n      _prefetchedFacetedUniqueValuesCache: null,\n      _prefetchedFacetedMinMaxValuesCache: null,\n    }\n\n    if (strategy === 'client') {\n      // Define prefetch methods with access to the column instance\n      column.prefetchOptions = async (): Promise<void> => {\n        if (!column._prefetchedOptionsCache) {\n          await new Promise((resolve) =>\n            setTimeout(() => {\n              const options = getOptions()\n              column._prefetchedOptionsCache = options\n              // console.log(`Prefetched options for ${columnConfig.id}`)\n              resolve(undefined)\n            }, 0),\n          )\n        }\n      }\n\n      column.prefetchValues = async (): Promise<void> => {\n        if (!column._prefetchedValuesCache) {\n          await new Promise((resolve) =>\n            setTimeout(() => {\n              const values = getValues()\n              column._prefetchedValuesCache = values\n              // console.log(`Prefetched values for ${columnConfig.id}`)\n              resolve(undefined)\n            }, 0),\n          )\n        }\n      }\n\n      column.prefetchFacetedUniqueValues = async (): Promise<void> => {\n        if (!column._prefetchedFacetedUniqueValuesCache) {\n          await new Promise((resolve) =>\n            setTimeout(() => {\n              const facetedMap = getUniqueValues()\n              column._prefetchedFacetedUniqueValuesCache = facetedMap ?? null\n              // console.log(\n              //   `Prefetched faceted unique values for ${columnConfig.id}`,\n              // )\n              resolve(undefined)\n            }, 0),\n          )\n        }\n      }\n\n      column.prefetchFacetedMinMaxValues = async (): Promise<void> => {\n        if (!column._prefetchedFacetedMinMaxValuesCache) {\n          await new Promise((resolve) =>\n            setTimeout(() => {\n              const value = getMinMaxValues()\n              column._prefetchedFacetedMinMaxValuesCache = value ?? null\n              // console.log(\n              //   `Prefetched faceted min/max values for ${columnConfig.id}`,\n              // )\n              resolve(undefined)\n            }, 0),\n          )\n        }\n      }\n    }\n\n    return column\n  })\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/core/filters.ts"
    },
    {
      "path": "registry/data-table-filter/core/operators.ts",
      "content": "import { type Locale, t } from '../lib/i18n'\nimport type {\n  ColumnDataType,\n  FilterDetails,\n  FilterOperatorTarget,\n  FilterOperators,\n  FilterTypeOperatorDetails,\n  FilterValues,\n} from './types'\n\nexport const DEFAULT_OPERATORS: Record<\n  ColumnDataType,\n  Record<FilterOperatorTarget, FilterOperators[ColumnDataType]>\n> = {\n  text: {\n    single: 'contains',\n    multiple: 'contains',\n  },\n  number: {\n    single: 'is',\n    multiple: 'is between',\n  },\n  date: {\n    single: 'is',\n    multiple: 'is between',\n  },\n  option: {\n    single: 'is',\n    multiple: 'is any of',\n  },\n  multiOption: {\n    single: 'include',\n    multiple: 'include any of',\n  },\n}\n\n/* Details for all the filter operators for option data type */\nexport const optionFilterOperators = {\n  is: {\n    key: 'filters.option.is',\n    value: 'is',\n    target: 'single',\n    singularOf: 'is any of',\n    relativeOf: 'is not',\n    isNegated: false,\n    negation: 'is not',\n  },\n  'is not': {\n    key: 'filters.option.isNot',\n    value: 'is not',\n    target: 'single',\n    singularOf: 'is none of',\n    relativeOf: 'is',\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is any of': {\n    key: 'filters.option.isAnyOf',\n    value: 'is any of',\n    target: 'multiple',\n    pluralOf: 'is',\n    relativeOf: 'is none of',\n    isNegated: false,\n    negation: 'is none of',\n  },\n  'is none of': {\n    key: 'filters.option.isNoneOf',\n    value: 'is none of',\n    target: 'multiple',\n    pluralOf: 'is not',\n    relativeOf: 'is any of',\n    isNegated: true,\n    negationOf: 'is any of',\n  },\n} as const satisfies FilterDetails<'option'>\n\n/* Details for all the filter operators for multi-option data type */\nexport const multiOptionFilterOperators = {\n  include: {\n    key: 'filters.multiOption.include',\n    value: 'include',\n    target: 'single',\n    singularOf: 'include any of',\n    relativeOf: 'exclude',\n    isNegated: false,\n    negation: 'exclude',\n  },\n  exclude: {\n    key: 'filters.multiOption.exclude',\n    value: 'exclude',\n    target: 'single',\n    singularOf: 'exclude if any of',\n    relativeOf: 'include',\n    isNegated: true,\n    negationOf: 'include',\n  },\n  'include any of': {\n    key: 'filters.multiOption.includeAnyOf',\n    value: 'include any of',\n    target: 'multiple',\n    pluralOf: 'include',\n    relativeOf: ['exclude if all', 'include all of', 'exclude if any of'],\n    isNegated: false,\n    negation: 'exclude if all',\n  },\n  'exclude if all': {\n    key: 'filters.multiOption.excludeIfAll',\n    value: 'exclude if all',\n    target: 'multiple',\n    pluralOf: 'exclude',\n    relativeOf: ['include any of', 'include all of', 'exclude if any of'],\n    isNegated: true,\n    negationOf: 'include any of',\n  },\n  'include all of': {\n    key: 'filters.multiOption.includeAllOf',\n    value: 'include all of',\n    target: 'multiple',\n    pluralOf: 'include',\n    relativeOf: ['include any of', 'exclude if all', 'exclude if any of'],\n    isNegated: false,\n    negation: 'exclude if any of',\n  },\n  'exclude if any of': {\n    key: 'filters.multiOption.excludeIfAnyOf',\n    value: 'exclude if any of',\n    target: 'multiple',\n    pluralOf: 'exclude',\n    relativeOf: ['include any of', 'exclude if all', 'include all of'],\n    isNegated: true,\n    negationOf: 'include all of',\n  },\n} as const satisfies FilterDetails<'multiOption'>\n\n/* Details for all the filter operators for date data type */\nexport const dateFilterOperators = {\n  is: {\n    key: 'filters.date.is',\n    value: 'is',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: 'is after',\n    isNegated: false,\n    negation: 'is before',\n  },\n  'is not': {\n    key: 'filters.date.isNot',\n    value: 'is not',\n    target: 'single',\n    singularOf: 'is not between',\n    relativeOf: [\n      'is',\n      'is before',\n      'is on or after',\n      'is after',\n      'is on or before',\n    ],\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is before': {\n    key: 'filters.date.isBefore',\n    value: 'is before',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is on or after',\n      'is after',\n      'is on or before',\n    ],\n    isNegated: false,\n    negation: 'is on or after',\n  },\n  'is on or after': {\n    key: 'filters.date.isOnOrAfter',\n    value: 'is on or after',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: ['is', 'is not', 'is before', 'is after', 'is on or before'],\n    isNegated: false,\n    negation: 'is before',\n  },\n  'is after': {\n    key: 'filters.date.isAfter',\n    value: 'is after',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is before',\n      'is on or after',\n      'is on or before',\n    ],\n    isNegated: false,\n    negation: 'is on or before',\n  },\n  'is on or before': {\n    key: 'filters.date.isOnOrBefore',\n    value: 'is on or before',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: ['is', 'is not', 'is after', 'is on or after', 'is before'],\n    isNegated: false,\n    negation: 'is after',\n  },\n  'is between': {\n    key: 'filters.date.isBetween',\n    value: 'is between',\n    target: 'multiple',\n    pluralOf: 'is',\n    relativeOf: 'is not between',\n    isNegated: false,\n    negation: 'is not between',\n  },\n  'is not between': {\n    key: 'filters.date.isNotBetween',\n    value: 'is not between',\n    target: 'multiple',\n    pluralOf: 'is not',\n    relativeOf: 'is between',\n    isNegated: true,\n    negationOf: 'is between',\n  },\n} as const satisfies FilterDetails<'date'>\n\n/* Details for all the filter operators for text data type */\nexport const textFilterOperators = {\n  contains: {\n    key: 'filters.text.contains',\n    value: 'contains',\n    target: 'single',\n    relativeOf: 'does not contain',\n    isNegated: false,\n    negation: 'does not contain',\n  },\n  'does not contain': {\n    key: 'filters.text.doesNotContain',\n    value: 'does not contain',\n    target: 'single',\n    relativeOf: 'contains',\n    isNegated: true,\n    negationOf: 'contains',\n  },\n} as const satisfies FilterDetails<'text'>\n\n/* Details for all the filter operators for number data type */\nexport const numberFilterOperators = {\n  is: {\n    key: 'filters.number.is',\n    value: 'is',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is not',\n  },\n  'is not': {\n    key: 'filters.number.isNot',\n    value: 'is not',\n    target: 'single',\n    singularOf: 'is not between',\n    relativeOf: [\n      'is',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: true,\n    negationOf: 'is',\n  },\n  'is greater than': {\n    key: 'filters.number.greaterThan',\n    value: 'is greater than',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is less than or equal to',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is less than or equal to',\n  },\n  'is greater than or equal to': {\n    key: 'filters.number.greaterThanOrEqual',\n    value: 'is greater than or equal to',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is less than',\n    ],\n    isNegated: false,\n    negation: 'is less than or equal to',\n  },\n  'is less than': {\n    key: 'filters.number.lessThan',\n    value: 'is less than',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than or equal to',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is greater than',\n  },\n  'is less than or equal to': {\n    key: 'filters.number.lessThanOrEqual',\n    value: 'is less than or equal to',\n    target: 'single',\n    singularOf: 'is between',\n    relativeOf: [\n      'is',\n      'is not',\n      'is greater than',\n      'is less than',\n      'is greater than or equal to',\n    ],\n    isNegated: false,\n    negation: 'is greater than or equal to',\n  },\n  'is between': {\n    key: 'filters.number.isBetween',\n    value: 'is between',\n    target: 'multiple',\n    pluralOf: 'is',\n    relativeOf: 'is not between',\n    isNegated: false,\n    negation: 'is not between',\n  },\n  'is not between': {\n    key: 'filters.number.isNotBetween',\n    value: 'is not between',\n    target: 'multiple',\n    pluralOf: 'is not',\n    relativeOf: 'is between',\n    isNegated: true,\n    negationOf: 'is between',\n  },\n} as const satisfies FilterDetails<'number'>\n\nexport const filterTypeOperatorDetails: FilterTypeOperatorDetails = {\n  text: textFilterOperators,\n  number: numberFilterOperators,\n  date: dateFilterOperators,\n  option: optionFilterOperators,\n  multiOption: multiOptionFilterOperators,\n}\n\n/*\n *\n * Determines the new operator for a filter based on the current operator, old and new filter values.\n *\n * This handles cases where the filter values have transitioned from a single value to multiple values (or vice versa),\n * and the current operator needs to be transitioned to its plural form (or singular form).\n *\n * For example, if the current operator is 'is', and the new filter values have a length of 2, the\n * new operator would be 'is any of'.\n *\n */\nexport function determineNewOperator<TType extends ColumnDataType>(\n  type: TType,\n  oldVals: FilterValues<TType>,\n  nextVals: FilterValues<TType>,\n  currentOperator: FilterOperators[TType],\n): FilterOperators[TType] {\n  const a =\n    Array.isArray(oldVals) && Array.isArray(oldVals[0])\n      ? oldVals[0].length\n      : oldVals.length\n  const b =\n    Array.isArray(nextVals) && Array.isArray(nextVals[0])\n      ? nextVals[0].length\n      : nextVals.length\n\n  // If filter size has not transitioned from single to multiple (or vice versa)\n  // or is unchanged, return the current operator.\n  if (a === b || (a >= 2 && b >= 2) || (a <= 1 && b <= 1))\n    return currentOperator\n\n  const opDetails = filterTypeOperatorDetails[type][currentOperator]\n\n  // Handle transition from single to multiple filter values.\n  if (a < b && b >= 2) return opDetails.singularOf ?? currentOperator\n  // Handle transition from multiple to single filter values.\n  if (a > b && b <= 1) return opDetails.pluralOf ?? currentOperator\n  return currentOperator\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/core/operators.ts"
    },
    {
      "path": "registry/data-table-filter/core/types.ts",
      "content": "import type { LucideIcon } from 'lucide-react'\n\n/*\n * # GENERAL NOTES:\n *\n * ## GENERICS:\n *\n * TData is the shape of a single row in your data table.\n * TVal is the shape of the underlying value for a column.\n * TType is the type (kind) of the column.\n *\n */\n\nexport type ElementType<T> = T extends (infer U)[] ? U : T\n\nexport type Nullable<T> = T | null | undefined\n\n/*\n * The model of a column option.\n * Used for representing underlying column values of type `option` or `multiOption`.\n */\nexport interface ColumnOption {\n  /* The label to display for the option. */\n  label: string\n  /* The internal value of the option. */\n  value: string\n  /* An optional icon to display next to the label. */\n  icon?: React.ReactElement | React.ElementType\n}\n\nexport interface ColumnOptionExtended extends ColumnOption {\n  selected?: boolean\n  count?: number\n}\n\n/*\n * Represents the data type (kind) of a column.\n */\nexport type ColumnDataType =\n  /* The column value is a string that should be searchable. */\n  | 'text'\n  | 'number'\n  | 'date'\n  /* The column value can be a single value from a list of options. */\n  | 'option'\n  /* The column value can be zero or more values from a list of options. */\n  | 'multiOption'\n\n/*\n * Represents the data type (kind) of option and multi-option columns.\n */\nexport type OptionBasedColumnDataType = Extract<\n  ColumnDataType,\n  'option' | 'multiOption'\n>\n\n/*\n * Maps a ColumnDataType to it's primitive type (i.e. string, number, etc.).\n */\nexport type ColumnDataNativeMap = {\n  text: string\n  number: number\n  date: Date\n  option: string\n  multiOption: string[]\n}\n\n/*\n * Represents the value of a column filter.\n * Contigent on the filtered column's data type.\n */\nexport type FilterValues<T extends ColumnDataType> = Array<\n  ElementType<ColumnDataNativeMap[T]>\n>\n\n/*\n * An accessor function for a column's data.\n * Uses the original row data as an argument.\n */\nexport type TAccessorFn<TData, TVal = unknown> = (data: TData) => TVal\n\n/*\n * Used by `option` and `multiOption` columns.\n * Transforms the underlying column value into a valid ColumnOption.\n */\nexport type TTransformOptionFn<TVal = unknown> = (\n  value: ElementType<NonNullable<TVal>>,\n) => ColumnOption\n\n/*\n * Used by `option` and `multiOption` columns.\n * A custom ordering function when sorting a column's options.\n */\nexport type TOrderFn<TVal = unknown> = (\n  a: ElementType<NonNullable<TVal>>,\n  b: ElementType<NonNullable<TVal>>,\n) => number\n\n/*\n * The configuration for a column.\n */\nexport type ColumnConfig<\n  TData,\n  TType extends ColumnDataType = any,\n  TVal = unknown,\n  TId extends string = string,\n> = {\n  id: TId\n  accessor: TAccessorFn<TData, TVal>\n  displayName: string\n  icon: LucideIcon\n  type: TType\n  options?: TType extends OptionBasedColumnDataType ? ColumnOption[] : never\n  facetedOptions?: TType extends OptionBasedColumnDataType\n    ? Map<string, number>\n    : never\n  min?: TType extends 'number' ? number : never\n  max?: TType extends 'number' ? number : never\n  transformOptionFn?: TType extends OptionBasedColumnDataType\n    ? TTransformOptionFn<TVal>\n    : never\n  orderFn?: TType extends OptionBasedColumnDataType ? TOrderFn<TVal> : never\n}\n\nexport type OptionColumnId<T> = T extends ColumnConfig<\n  infer TData,\n  'option' | 'multiOption',\n  infer TVal,\n  infer TId\n>\n  ? TId\n  : never\n\nexport type OptionColumnIds<\n  T extends ReadonlyArray<ColumnConfig<any, any, any, any>>,\n> = {\n  [K in keyof T]: OptionColumnId<T[K]>\n}[number]\n\nexport type NumberColumnId<T> = T extends ColumnConfig<\n  infer TData,\n  'number',\n  infer TVal,\n  infer TId\n>\n  ? TId\n  : never\n\nexport type NumberColumnIds<\n  T extends ReadonlyArray<ColumnConfig<any, any, any, any>>,\n> = {\n  [K in keyof T]: NumberColumnId<T[K]>\n}[number]\n\n/*\n * Describes a helper function for creating column configurations.\n */\nexport type ColumnConfigHelper<TData> = {\n  accessor: <\n    TAccessor extends TAccessorFn<TData>,\n    TType extends ColumnDataType,\n    TVal extends ReturnType<TAccessor>,\n  >(\n    accessor: TAccessor,\n    config?: Omit<ColumnConfig<TData, TType, TVal>, 'accessor'>,\n  ) => ColumnConfig<TData, TType, unknown>\n}\n\nexport type DataTableFilterConfig<TData> = {\n  data: TData[]\n  columns: ColumnConfig<TData>[]\n}\n\nexport type ColumnProperties<TData, TVal> = {\n  getOptions: () => ColumnOption[]\n  getValues: () => ElementType<NonNullable<TVal>>[]\n  getFacetedUniqueValues: () => Map<string, number> | undefined\n  getFacetedMinMaxValues: () => [number, number] | undefined\n  prefetchOptions: () => Promise<void> // Prefetch options\n  prefetchValues: () => Promise<void> // Prefetch values\n  prefetchFacetedUniqueValues: () => Promise<void> // Prefetch faceted unique values\n  prefetchFacetedMinMaxValues: () => Promise<void> // Prefetch faceted min/max values\n}\n\nexport type ColumnPrivateProperties<TData, TVal> = {\n  _prefetchedOptionsCache: ColumnOption[] | null\n  _prefetchedValuesCache: ElementType<NonNullable<TVal>>[] | null\n  _prefetchedFacetedUniqueValuesCache: Map<string, number> | null\n  _prefetchedFacetedMinMaxValuesCache: [number, number] | null\n}\n\nexport type Column<\n  TData,\n  TType extends ColumnDataType = any,\n  TVal = unknown,\n> = ColumnConfig<TData, TType, TVal> &\n  ColumnProperties<TData, TVal> &\n  ColumnPrivateProperties<TData, TVal>\n\n/*\n * Describes the available actions on column filters.\n * Includes both column-specific and global actions, ultimately acting on the column filters.\n */\nexport interface DataTableFilterActions {\n  addFilterValue: <TData, TType extends OptionBasedColumnDataType>(\n    column: Column<TData, TType>,\n    values: FilterModel<TType>['values'],\n  ) => void\n\n  removeFilterValue: <TData, TType extends OptionBasedColumnDataType>(\n    column: Column<TData, TType>,\n    value: FilterModel<TType>['values'],\n  ) => void\n\n  setFilterValue: <TData, TType extends ColumnDataType>(\n    column: Column<TData, TType>,\n    values: FilterModel<TType>['values'],\n  ) => void\n\n  setFilterOperator: <TType extends ColumnDataType>(\n    columnId: string,\n    operator: FilterModel<TType>['operator'],\n  ) => void\n\n  removeFilter: (columnId: string) => void\n\n  removeAllFilters: () => void\n}\n\nexport type FilterStrategy = 'client' | 'server'\n\n/* Operators for text data */\nexport type TextFilterOperator = 'contains' | 'does not contain'\n\n/* Operators for number data */\nexport type NumberFilterOperator =\n  | 'is'\n  | 'is not'\n  | 'is less than'\n  | 'is greater than or equal to'\n  | 'is greater than'\n  | 'is less than or equal to'\n  | 'is between'\n  | 'is not between'\n\n/* Operators for date data */\nexport type DateFilterOperator =\n  | 'is'\n  | 'is not'\n  | 'is before'\n  | 'is on or after'\n  | 'is after'\n  | 'is on or before'\n  | 'is between'\n  | 'is not between'\n\n/* Operators for option data */\nexport type OptionFilterOperator = 'is' | 'is not' | 'is any of' | 'is none of'\n\n/* Operators for multi-option data */\nexport type MultiOptionFilterOperator =\n  | 'include'\n  | 'exclude'\n  | 'include any of'\n  | 'include all of'\n  | 'exclude if any of'\n  | 'exclude if all'\n\n/* Maps filter operators to their respective data types */\nexport type FilterOperators = {\n  text: TextFilterOperator\n  number: NumberFilterOperator\n  date: DateFilterOperator\n  option: OptionFilterOperator\n  multiOption: MultiOptionFilterOperator\n}\n\n/*\n *\n * FilterValue is a type that represents a filter value for a specific column.\n *\n * It consists of:\n * - Operator: The operator to be used for the filter.\n * - Values: An array of values to be used for the filter.\n *\n */\nexport type FilterModel<TType extends ColumnDataType = any> = {\n  columnId: string\n  type: TType\n  operator: FilterOperators[TType]\n  values: FilterValues<TType>\n}\n\nexport type FiltersState = Array<FilterModel>\n\n/*\n * FilterDetails is a type that represents the details of all the filter operators for a specific column data type.\n */\nexport type FilterDetails<T extends ColumnDataType> = {\n  [key in FilterOperators[T]]: FilterOperatorDetails<key, T>\n}\n\nexport type FilterOperatorTarget = 'single' | 'multiple'\n\nexport type FilterOperatorDetailsBase<\n  OperatorValue,\n  T extends ColumnDataType,\n> = {\n  /* The i18n key for the operator. */\n  key: string\n  /* The operator value. Usually the string representation of the operator. */\n  value: OperatorValue\n  /* How much data the operator applies to. */\n  target: FilterOperatorTarget\n  /* The plural form of the operator, if applicable. */\n  singularOf?: FilterOperators[T]\n  /* The singular form of the operator, if applicable. */\n  pluralOf?: FilterOperators[T]\n  /* All related operators. Normally, all the operators which share the same target. */\n  relativeOf: FilterOperators[T] | Array<FilterOperators[T]>\n  /* Whether the operator is negated. */\n  isNegated: boolean\n  /* If the operator is not negated, this provides the negated equivalent. */\n  negation?: FilterOperators[T]\n  /* If the operator is negated, this provides the positive equivalent. */\n  negationOf?: FilterOperators[T]\n}\n\n/*\n *\n * FilterOperatorDetails is a type that provides details about a filter operator for a specific column data type.\n * It extends FilterOperatorDetailsBase with additional logic and contraints on the defined properties.\n *\n */\nexport type FilterOperatorDetails<\n  OperatorValue,\n  T extends ColumnDataType,\n> = FilterOperatorDetailsBase<OperatorValue, T> &\n  (\n    | { singularOf?: never; pluralOf?: never }\n    | { target: 'single'; singularOf: FilterOperators[T]; pluralOf?: never }\n    | { target: 'multiple'; singularOf?: never; pluralOf: FilterOperators[T] }\n  ) &\n  (\n    | { isNegated: false; negation: FilterOperators[T]; negationOf?: never }\n    | { isNegated: true; negation?: never; negationOf: FilterOperators[T] }\n  )\n\n/* Maps column data types to their respective filter operator details */\nexport type FilterTypeOperatorDetails = {\n  [key in ColumnDataType]: FilterDetails<key>\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/core/types.ts"
    },
    {
      "path": "registry/data-table-filter/hooks/use-data-table-filters.tsx",
      "content": "'use client'\n\nimport type React from 'react'\nimport { useMemo, useState } from 'react'\nimport { createColumns } from '../core/filters'\nimport { DEFAULT_OPERATORS, determineNewOperator } from '../core/operators'\nimport type {\n  ColumnConfig,\n  ColumnDataType,\n  ColumnOption,\n  DataTableFilterActions,\n  FilterModel,\n  FilterStrategy,\n  FiltersState,\n  NumberColumnIds,\n  OptionBasedColumnDataType,\n  OptionColumnIds,\n} from '../core/types'\nimport { uniq } from '../lib/array'\nimport { addUniq, removeUniq } from '../lib/array'\nimport {\n  createDateFilterValue,\n  createNumberFilterValue,\n  isColumnOptionArray,\n  isColumnOptionMap,\n  isMinMaxTuple,\n} from '../lib/helpers'\n\nexport interface DataTableFiltersOptions<\n  TData,\n  TColumns extends ReadonlyArray<ColumnConfig<TData, any, any, any>>,\n  TStrategy extends FilterStrategy,\n> {\n  strategy: TStrategy\n  data: TData[]\n  columnsConfig: TColumns\n  defaultFilters?: FiltersState\n  filters?: FiltersState\n  onFiltersChange?: React.Dispatch<React.SetStateAction<FiltersState>>\n  options?: Partial<\n    Record<OptionColumnIds<TColumns>, ColumnOption[] | undefined>\n  >\n  faceted?: Partial<\n    | Record<OptionColumnIds<TColumns>, Map<string, number> | undefined>\n    | Record<NumberColumnIds<TColumns>, [number, number] | undefined>\n  >\n}\n\nexport function useDataTableFilters<\n  TData,\n  TColumns extends ReadonlyArray<ColumnConfig<TData, any, any, any>>,\n  TStrategy extends FilterStrategy,\n>({\n  strategy,\n  data,\n  columnsConfig,\n  defaultFilters,\n  filters: externalFilters,\n  onFiltersChange,\n  options,\n  faceted,\n}: DataTableFiltersOptions<TData, TColumns, TStrategy>) {\n  const [internalFilters, setInternalFilters] = useState<FiltersState>(\n    defaultFilters ?? [],\n  )\n\n  if (\n    (externalFilters && !onFiltersChange) ||\n    (!externalFilters && onFiltersChange)\n  ) {\n    throw new Error(\n      'If using controlled state, you must specify both filters and onFiltersChange.',\n    )\n  }\n\n  const filters = externalFilters ?? internalFilters\n  const setFilters = onFiltersChange ?? setInternalFilters\n\n  // Convert ColumnConfig to Column, applying options and faceted options if provided\n  const columns = useMemo(() => {\n    const enhancedConfigs = columnsConfig.map((config) => {\n      let final = config\n\n      // Set options, if exists\n      if (\n        options &&\n        (config.type === 'option' || config.type === 'multiOption')\n      ) {\n        const optionsInput = options[config.id as OptionColumnIds<TColumns>]\n        if (!optionsInput || !isColumnOptionArray(optionsInput)) return config\n\n        final = { ...final, options: optionsInput }\n      }\n\n      // Set faceted options, if exists\n      if (\n        faceted &&\n        (config.type === 'option' || config.type === 'multiOption')\n      ) {\n        const facetedOptionsInput =\n          faceted[config.id as OptionColumnIds<TColumns>]\n        if (!facetedOptionsInput || !isColumnOptionMap(facetedOptionsInput))\n          return config\n\n        final = { ...final, facetedOptions: facetedOptionsInput }\n      }\n\n      // Set faceted min/max values, if exists\n      if (config.type === 'number' && faceted) {\n        const minMaxTuple = faceted[config.id as NumberColumnIds<TColumns>]\n        if (!minMaxTuple || !isMinMaxTuple(minMaxTuple)) return config\n\n        final = {\n          ...final,\n          min: minMaxTuple[0],\n          max: minMaxTuple[1],\n        }\n      }\n\n      return final\n    })\n\n    return createColumns(data, enhancedConfigs, strategy)\n  }, [data, columnsConfig, options, faceted, strategy])\n\n  const actions: DataTableFilterActions = useMemo(\n    () => ({\n      addFilterValue<TData, TType extends OptionBasedColumnDataType>(\n        column: ColumnConfig<TData, TType>,\n        values: FilterModel<TType>['values'],\n      ) {\n        if (column.type === 'option') {\n          setFilters((prev) => {\n            const filter = prev.find((f) => f.columnId === column.id)\n            const isColumnFiltered = filter && filter.values.length > 0\n            if (!isColumnFiltered) {\n              return [\n                ...prev,\n                {\n                  columnId: column.id,\n                  type: column.type,\n                  operator:\n                    values.length > 1\n                      ? DEFAULT_OPERATORS[column.type].multiple\n                      : DEFAULT_OPERATORS[column.type].single,\n                  values,\n                },\n              ]\n            }\n            const oldValues = filter.values\n            const newValues = addUniq(filter.values, values)\n            const newOperator = determineNewOperator(\n              'option',\n              oldValues,\n              newValues,\n              filter.operator,\n            )\n            return prev.map((f) =>\n              f.columnId === column.id\n                ? {\n                    columnId: column.id,\n                    type: column.type,\n                    operator: newOperator,\n                    values: newValues,\n                  }\n                : f,\n            )\n          })\n          return\n        }\n        if (column.type === 'multiOption') {\n          setFilters((prev) => {\n            const filter = prev.find((f) => f.columnId === column.id)\n            const isColumnFiltered = filter && filter.values.length > 0\n            if (!isColumnFiltered) {\n              return [\n                ...prev,\n                {\n                  columnId: column.id,\n                  type: column.type,\n                  operator:\n                    values.length > 1\n                      ? DEFAULT_OPERATORS[column.type].multiple\n                      : DEFAULT_OPERATORS[column.type].single,\n                  values,\n                },\n              ]\n            }\n            const oldValues = filter.values\n            const newValues = addUniq(filter.values, values)\n            const newOperator = determineNewOperator(\n              'multiOption',\n              oldValues,\n              newValues,\n              filter.operator,\n            )\n            if (newValues.length === 0) {\n              return prev.filter((f) => f.columnId !== column.id)\n            }\n            return prev.map((f) =>\n              f.columnId === column.id\n                ? {\n                    columnId: column.id,\n                    type: column.type,\n                    operator: newOperator,\n                    values: newValues,\n                  }\n                : f,\n            )\n          })\n          return\n        }\n        throw new Error(\n          '[data-table-filter] addFilterValue() is only supported for option columns',\n        )\n      },\n      removeFilterValue<TData, TType extends OptionBasedColumnDataType>(\n        column: ColumnConfig<TData, TType>,\n        value: FilterModel<TType>['values'],\n      ) {\n        if (column.type === 'option') {\n          setFilters((prev) => {\n            const filter = prev.find((f) => f.columnId === column.id)\n            const isColumnFiltered = filter && filter.values.length > 0\n            if (!isColumnFiltered) {\n              return [...prev]\n            }\n            const newValues = removeUniq(filter.values, value)\n            const oldValues = filter.values\n            const newOperator = determineNewOperator(\n              'option',\n              oldValues,\n              newValues,\n              filter.operator,\n            )\n            if (newValues.length === 0) {\n              return prev.filter((f) => f.columnId !== column.id)\n            }\n            return prev.map((f) =>\n              f.columnId === column.id\n                ? {\n                    columnId: column.id,\n                    type: column.type,\n                    operator: newOperator,\n                    values: newValues,\n                  }\n                : f,\n            )\n          })\n          return\n        }\n        if (column.type === 'multiOption') {\n          setFilters((prev) => {\n            const filter = prev.find((f) => f.columnId === column.id)\n            const isColumnFiltered = filter && filter.values.length > 0\n            if (!isColumnFiltered) {\n              return [...prev]\n            }\n            const newValues = removeUniq(filter.values, value)\n            const oldValues = filter.values\n            const newOperator = determineNewOperator(\n              'multiOption',\n              oldValues,\n              newValues,\n              filter.operator,\n            )\n            if (newValues.length === 0) {\n              return prev.filter((f) => f.columnId !== column.id)\n            }\n            return prev.map((f) =>\n              f.columnId === column.id\n                ? {\n                    columnId: column.id,\n                    type: column.type,\n                    operator: newOperator,\n                    values: newValues,\n                  }\n                : f,\n            )\n          })\n          return\n        }\n        throw new Error(\n          '[data-table-filter] removeFilterValue() is only supported for option columns',\n        )\n      },\n      setFilterValue<TData, TType extends ColumnDataType>(\n        column: ColumnConfig<TData, TType>,\n        values: FilterModel<TType>['values'],\n      ) {\n        setFilters((prev) => {\n          const filter = prev.find((f) => f.columnId === column.id)\n          const isColumnFiltered = filter && filter.values.length > 0\n          const newValues =\n            column.type === 'number'\n              ? createNumberFilterValue(values as number[])\n              : column.type === 'date'\n                ? createDateFilterValue(\n                    values as [Date, Date] | [Date] | [] | undefined,\n                  )\n                : uniq(values)\n          if (newValues.length === 0) return prev\n          if (!isColumnFiltered) {\n            return [\n              ...prev,\n              {\n                columnId: column.id,\n                type: column.type,\n                operator:\n                  values.length > 1\n                    ? DEFAULT_OPERATORS[column.type].multiple\n                    : DEFAULT_OPERATORS[column.type].single,\n                values: newValues,\n              },\n            ]\n          }\n          const oldValues = filter.values\n          const newOperator = determineNewOperator(\n            column.type,\n            oldValues,\n            newValues,\n            filter.operator,\n          )\n          const newFilter = {\n            columnId: column.id,\n            type: column.type,\n            operator: newOperator,\n            values: newValues as any,\n          } satisfies FilterModel<TType>\n          return prev.map((f) => (f.columnId === column.id ? newFilter : f))\n        })\n      },\n      setFilterOperator<TType extends ColumnDataType>(\n        columnId: string,\n        operator: FilterModel<TType>['operator'],\n      ) {\n        setFilters((prev) =>\n          prev.map((f) => (f.columnId === columnId ? { ...f, operator } : f)),\n        )\n      },\n      removeFilter(columnId: string) {\n        setFilters((prev) => prev.filter((f) => f.columnId !== columnId))\n      },\n      removeAllFilters() {\n        setFilters([])\n      },\n    }),\n    [setFilters],\n  )\n\n  return { columns, filters, actions, strategy } // columns is Column<TData>[]\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/hooks/use-data-table-filters.tsx"
    },
    {
      "path": "registry/data-table-filter/hooks/use-debounce-callback.tsx",
      "content": "/*\n * Source: https://usehooks-ts.com/react-hook/use-debounce-callback\n */\n\nimport { useEffect, useMemo, useRef } from 'react'\nimport { debounce } from '../lib/debounce'\nimport { useUnmount } from './use-unmount'\n\ntype DebounceOptions = {\n  leading?: boolean\n  trailing?: boolean\n  maxWait?: number\n}\n\ntype ControlFunctions = {\n  cancel: () => void\n  flush: () => void\n  isPending: () => boolean\n}\n\nexport type DebouncedState<T extends (...args: any) => ReturnType<T>> = ((\n  ...args: Parameters<T>\n) => ReturnType<T> | undefined) &\n  ControlFunctions\n\nexport function useDebounceCallback<T extends (...args: any) => ReturnType<T>>(\n  func: T,\n  delay = 500,\n  options?: DebounceOptions,\n): DebouncedState<T> {\n  const debouncedFunc = useRef<ReturnType<typeof debounce>>(null)\n\n  useUnmount(() => {\n    if (debouncedFunc.current) {\n      debouncedFunc.current.cancel()\n    }\n  })\n\n  const debounced = useMemo(() => {\n    const debouncedFuncInstance = debounce(func, delay, options)\n\n    const wrappedFunc: DebouncedState<T> = (...args: Parameters<T>) => {\n      return debouncedFuncInstance(...args)\n    }\n\n    wrappedFunc.cancel = () => {\n      debouncedFuncInstance.cancel()\n    }\n\n    wrappedFunc.isPending = () => {\n      return !!debouncedFunc.current\n    }\n\n    wrappedFunc.flush = () => {\n      return debouncedFuncInstance.flush()\n    }\n\n    return wrappedFunc\n  }, [func, delay, options])\n\n  // Update the debounced function ref whenever func, wait, or options change\n  useEffect(() => {\n    debouncedFunc.current = debounce(func, delay, options)\n  }, [func, delay, options])\n\n  return debounced\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/hooks/use-debounce-callback.tsx"
    },
    {
      "path": "registry/data-table-filter/hooks/use-unmount.tsx",
      "content": "/*\n * Source: https://usehooks-ts.com/react-hook/use-unmount\n */\n\nimport { useEffect, useRef } from 'react'\n\nexport function useUnmount(func: () => void) {\n  const funcRef = useRef(func)\n\n  funcRef.current = func\n\n  useEffect(\n    () => () => {\n      funcRef.current()\n    },\n    [],\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/hooks/use-unmount.tsx"
    },
    {
      "path": "registry/data-table-filter/lib/array.ts",
      "content": "export function intersection<T>(a: T[], b: T[]): T[] {\n  return a.filter((x) => b.includes(x))\n}\n\n/**\n * Computes a stable hash string for any value using deep inspection.\n * This function recursively builds a string for primitives, arrays, and objects.\n * It uses a cache (WeakMap) to avoid rehashing the same object twice, which is\n * particularly beneficial if an object appears in multiple places.\n */\nfunction deepHash(value: any, cache = new WeakMap<object, string>()): string {\n  // Handle primitives and null/undefined.\n  if (value === null) return 'null'\n  if (value === undefined) return 'undefined'\n  const type = typeof value\n  if (type === 'number' || type === 'boolean' || type === 'string') {\n    return `${type}:${value.toString()}`\n  }\n  if (type === 'function') {\n    // Note: using toString for functions.\n    return `function:${value.toString()}`\n  }\n\n  // For objects and arrays, use caching to avoid repeated work.\n  if (type === 'object') {\n    // If we’ve seen this object before, return the cached hash.\n    if (cache.has(value)) {\n      return cache.get(value)!\n    }\n    let hash: string\n    if (Array.isArray(value)) {\n      // Compute hash for each element in order.\n      hash = `array:[${value.map((v) => deepHash(v, cache)).join(',')}]`\n    } else {\n      // For objects, sort keys to ensure the representation is stable.\n      const keys = Object.keys(value).sort()\n      const props = keys\n        .map((k) => `${k}:${deepHash(value[k], cache)}`)\n        .join(',')\n      hash = `object:{${props}}`\n    }\n    cache.set(value, hash)\n    return hash\n  }\n\n  // Fallback if no case matched.\n  return `${type}:${value.toString()}`\n}\n\n/**\n * Performs deep equality check for any two values.\n * This recursively checks primitives, arrays, and plain objects.\n */\nfunction deepEqual(a: any, b: any): boolean {\n  // Check strict equality first.\n  if (a === b) return true\n  // If types differ, they’re not equal.\n  if (typeof a !== typeof b) return false\n  if (a === null || b === null || a === undefined || b === undefined)\n    return false\n\n  // Check arrays.\n  if (Array.isArray(a)) {\n    if (!Array.isArray(b) || a.length !== b.length) return false\n    for (let i = 0; i < a.length; i++) {\n      if (!deepEqual(a[i], b[i])) return false\n    }\n    return true\n  }\n\n  // Check objects.\n  if (typeof a === 'object') {\n    if (typeof b !== 'object') return false\n    const aKeys = Object.keys(a).sort()\n    const bKeys = Object.keys(b).sort()\n    if (aKeys.length !== bKeys.length) return false\n    for (let i = 0; i < aKeys.length; i++) {\n      if (aKeys[i] !== bKeys[i]) return false\n      if (!deepEqual(a[aKeys[i]], b[bKeys[i]])) return false\n    }\n    return true\n  }\n\n  // For any other types (should be primitives by now), use strict equality.\n  return false\n}\n\n/**\n * Returns a new array containing only the unique values from the input array.\n * Uniqueness is determined by deep equality.\n *\n * @param arr - The array of values to be filtered.\n * @returns A new array with duplicates removed.\n */\nexport function uniq<T>(arr: T[]): T[] {\n  // Use a Map where key is the deep hash and value is an array of items sharing the same hash.\n  const seen = new Map<string, T[]>()\n  const result: T[] = []\n\n  for (const item of arr) {\n    const hash = deepHash(item)\n    if (seen.has(hash)) {\n      // There is a potential duplicate; check the stored items with the same hash.\n      const itemsWithHash = seen.get(hash)!\n      let duplicateFound = false\n      for (const existing of itemsWithHash) {\n        if (deepEqual(existing, item)) {\n          duplicateFound = true\n          break\n        }\n      }\n      if (!duplicateFound) {\n        itemsWithHash.push(item)\n        result.push(item)\n      }\n    } else {\n      // First time this hash appears.\n      seen.set(hash, [item])\n      result.push(item)\n    }\n  }\n\n  return result\n}\n\nexport function take<T>(a: T[], n: number): T[] {\n  return a.slice(0, n)\n}\n\nexport function flatten<T>(a: T[][]): T[] {\n  return a.flat()\n}\n\nexport function addUniq<T>(arr: T[], values: T[]): T[] {\n  return uniq([...arr, ...values])\n}\n\nexport function removeUniq<T>(arr: T[], values: T[]): T[] {\n  return arr.filter((v) => !values.includes(v))\n}\n\nexport function isAnyOf<T>(value: T, values: T[]): boolean {\n  return values.includes(value)\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/lib/array.ts"
    },
    {
      "path": "registry/data-table-filter/lib/debounce.ts",
      "content": "type ControlFunctions = {\n  cancel: () => void\n  flush: () => void\n  isPending: () => boolean\n}\n\ntype DebounceOptions = {\n  leading?: boolean\n  trailing?: boolean\n  maxWait?: number\n}\n\nexport function debounce<T extends (...args: any[]) => any>(\n  func: T,\n  wait: number,\n  options: DebounceOptions = {},\n): ((...args: Parameters<T>) => ReturnType<T> | undefined) & ControlFunctions {\n  const { leading = false, trailing = true, maxWait } = options\n  let timeout: NodeJS.Timeout | null = null\n  let lastArgs: Parameters<T> | null = null\n  let lastThis: any\n  let result: ReturnType<T> | undefined\n  let lastCallTime: number | null = null\n  let lastInvokeTime = 0\n\n  const maxWaitTime = maxWait !== undefined ? Math.max(wait, maxWait) : null\n\n  function invokeFunc(time: number): ReturnType<T> | undefined {\n    if (lastArgs === null) return undefined\n    const args = lastArgs\n    const thisArg = lastThis\n    lastArgs = null\n    lastThis = null\n    lastInvokeTime = time\n    result = func.apply(thisArg, args)\n    return result\n  }\n\n  function shouldInvoke(time: number): boolean {\n    if (lastCallTime === null) return false\n    const timeSinceLastCall = time - lastCallTime\n    const timeSinceLastInvoke = time - lastInvokeTime\n    return (\n      lastCallTime === null ||\n      timeSinceLastCall >= wait ||\n      timeSinceLastCall < 0 ||\n      (maxWaitTime !== null && timeSinceLastInvoke >= maxWaitTime)\n    )\n  }\n\n  function startTimer(\n    pendingFunc: () => void,\n    waitTime: number,\n  ): NodeJS.Timeout {\n    return setTimeout(pendingFunc, waitTime)\n  }\n\n  function remainingWait(time: number): number {\n    if (lastCallTime === null) return wait\n    const timeSinceLastCall = time - lastCallTime\n    const timeSinceLastInvoke = time - lastInvokeTime\n    const timeWaiting = wait - timeSinceLastCall\n    return maxWaitTime !== null\n      ? Math.min(timeWaiting, maxWaitTime - timeSinceLastInvoke)\n      : timeWaiting\n  }\n\n  function timerExpired() {\n    const time = Date.now()\n    if (shouldInvoke(time)) {\n      return trailingEdge(time)\n    }\n    timeout = startTimer(timerExpired, remainingWait(time))\n  }\n\n  function leadingEdge(time: number): ReturnType<T> | undefined {\n    lastInvokeTime = time\n    timeout = startTimer(timerExpired, wait)\n    return leading ? invokeFunc(time) : undefined\n  }\n\n  function trailingEdge(time: number): ReturnType<T> | undefined {\n    timeout = null\n    if (trailing && lastArgs) {\n      return invokeFunc(time)\n    }\n    lastArgs = null\n    lastThis = null\n    return result\n  }\n\n  function debounced(\n    this: any,\n    ...args: Parameters<T>\n  ): ReturnType<T> | undefined {\n    const time = Date.now()\n    const isInvoking = shouldInvoke(time)\n\n    lastArgs = args\n    lastThis = this\n    lastCallTime = time\n\n    if (isInvoking) {\n      if (timeout === null) {\n        return leadingEdge(lastCallTime)\n      }\n      if (maxWaitTime !== null) {\n        timeout = startTimer(timerExpired, wait)\n        return invokeFunc(lastCallTime)\n      }\n    }\n    if (timeout === null) {\n      timeout = startTimer(timerExpired, wait)\n    }\n    return result\n  }\n\n  debounced.cancel = () => {\n    if (timeout !== null) {\n      clearTimeout(timeout)\n    }\n    lastInvokeTime = 0\n    lastArgs = null\n    lastThis = null\n    lastCallTime = null\n    timeout = null\n  }\n\n  debounced.flush = () => {\n    return timeout === null ? result : trailingEdge(Date.now())\n  }\n\n  debounced.isPending = () => {\n    return timeout !== null\n  }\n\n  return debounced\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/lib/debounce.ts"
    },
    {
      "path": "registry/data-table-filter/lib/filter-fns.ts",
      "content": "import {\n  endOfDay,\n  isAfter,\n  isBefore,\n  isSameDay,\n  isWithinInterval,\n  startOfDay,\n} from 'date-fns'\nimport { dateFilterOperators } from '../core/operators'\nimport type { FilterModel } from '../core/types'\nimport { intersection } from './array'\n\nexport function optionFilterFn<TData>(\n  inputData: string,\n  filterValue: FilterModel<'option'>,\n) {\n  if (!inputData) return false\n  if (filterValue.values.length === 0) return true\n\n  const value = inputData.toString().toLowerCase()\n\n  const found = !!filterValue.values.find((v) => v.toLowerCase() === value)\n\n  switch (filterValue.operator) {\n    case 'is':\n    case 'is any of':\n      return found\n    case 'is not':\n    case 'is none of':\n      return !found\n  }\n}\n\nexport function multiOptionFilterFn(\n  inputData: string[],\n  filterValue: FilterModel<'multiOption'>,\n) {\n  if (!inputData) return false\n\n  if (\n    filterValue.values.length === 0 ||\n    !filterValue.values[0] ||\n    filterValue.values[0].length === 0\n  )\n    return true\n\n  const values = inputData\n  const filterValues = filterValue.values\n\n  switch (filterValue.operator) {\n    case 'include':\n    case 'include any of':\n      return intersection(values, filterValues).length > 0\n    case 'exclude':\n      return intersection(values, filterValues).length === 0\n    case 'exclude if any of':\n      return !(intersection(values, filterValues).length > 0)\n    case 'include all of':\n      return intersection(values, filterValues).length === filterValues.length\n    case 'exclude if all':\n      return !(\n        intersection(values, filterValues).length === filterValues.length\n      )\n  }\n}\n\nexport function dateFilterFn<TData>(\n  inputData: Date,\n  filterValue: FilterModel<'date'>,\n) {\n  if (!filterValue || filterValue.values.length === 0) return true\n\n  if (\n    dateFilterOperators[filterValue.operator].target === 'single' &&\n    filterValue.values.length > 1\n  )\n    throw new Error('Singular operators require at most one filter value')\n\n  if (\n    filterValue.operator in ['is between', 'is not between'] &&\n    filterValue.values.length !== 2\n  )\n    throw new Error('Plural operators require two filter values')\n\n  const filterVals = filterValue.values\n  const d1 = filterVals[0]\n  const d2 = filterVals[1]\n\n  const value = inputData\n\n  switch (filterValue.operator) {\n    case 'is':\n      return isSameDay(value, d1)\n    case 'is not':\n      return !isSameDay(value, d1)\n    case 'is before':\n      return isBefore(value, startOfDay(d1))\n    case 'is on or after':\n      return isSameDay(value, d1) || isAfter(value, startOfDay(d1))\n    case 'is after':\n      return isAfter(value, startOfDay(d1))\n    case 'is on or before':\n      return isSameDay(value, d1) || isBefore(value, startOfDay(d1))\n    case 'is between':\n      return isWithinInterval(value, {\n        start: startOfDay(d1),\n        end: endOfDay(d2),\n      })\n    case 'is not between':\n      return !isWithinInterval(value, {\n        start: startOfDay(filterValue.values[0]),\n        end: endOfDay(filterValue.values[1]),\n      })\n  }\n}\n\nexport function textFilterFn<TData>(\n  inputData: string,\n  filterValue: FilterModel<'text'>,\n) {\n  if (!filterValue || filterValue.values.length === 0) return true\n\n  const value = inputData.toLowerCase().trim()\n  const filterStr = filterValue.values[0].toLowerCase().trim()\n\n  if (filterStr === '') return true\n\n  const found = value.includes(filterStr)\n\n  switch (filterValue.operator) {\n    case 'contains':\n      return found\n    case 'does not contain':\n      return !found\n  }\n}\n\nexport function numberFilterFn<TData>(\n  inputData: number,\n  filterValue: FilterModel<'number'>,\n) {\n  if (!filterValue || !filterValue.values || filterValue.values.length === 0) {\n    return true\n  }\n\n  const value = inputData\n  const filterVal = filterValue.values[0]\n\n  switch (filterValue.operator) {\n    case 'is':\n      return value === filterVal\n    case 'is not':\n      return value !== filterVal\n    case 'is greater than':\n      return value > filterVal\n    case 'is greater than or equal to':\n      return value >= filterVal\n    case 'is less than':\n      return value < filterVal\n    case 'is less than or equal to':\n      return value <= filterVal\n    case 'is between': {\n      const lowerBound = filterValue.values[0]\n      const upperBound = filterValue.values[1]\n      return value >= lowerBound && value <= upperBound\n    }\n    case 'is not between': {\n      const lowerBound = filterValue.values[0]\n      const upperBound = filterValue.values[1]\n      return value < lowerBound || value > upperBound\n    }\n    default:\n      return true\n  }\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/lib/filter-fns.ts"
    },
    {
      "path": "registry/data-table-filter/lib/memo.ts",
      "content": "export function memo<TDeps extends readonly any[], TResult>(\n  getDeps: () => TDeps,\n  compute: (deps: TDeps) => TResult,\n  options: { key: string },\n): () => TResult {\n  let prevDeps: TDeps | undefined\n  let cachedResult: TResult | undefined\n\n  return () => {\n    // console.log(`[memo] Calling memoized function: ${options.key}`)\n\n    const deps = getDeps()\n\n    // If no previous deps or deps have changed, recompute\n    if (!prevDeps || !shallowEqual(prevDeps, deps)) {\n      // console.log(`[memo] Cache MISS - ${options.key}`)\n      cachedResult = compute(deps)\n      prevDeps = deps\n    } else {\n      // console.log(`[memo] Cache HIT - ${options.key}`)\n    }\n\n    return cachedResult!\n  }\n}\n\nfunction shallowEqual<T>(arr1: readonly T[], arr2: readonly T[]): boolean {\n  if (arr1 === arr2) return true\n  if (arr1.length !== arr2.length) return false\n\n  for (let i = 0; i < arr1.length; i++) {\n    if (arr1[i] !== arr2[i]) return false\n  }\n  return true\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/lib/memo.ts"
    },
    {
      "path": "registry/data-table-filter/lib/helpers.ts",
      "content": "import { isBefore } from 'date-fns'\nimport type { Column, ColumnOption } from '../core/types'\n\nexport function getColumn<TData>(columns: Column<TData>[], id: string) {\n  const column = columns.find((c) => c.id === id)\n\n  if (!column) {\n    throw new Error(`Column with id ${id} not found`)\n  }\n\n  return column\n}\n\nexport function createNumberFilterValue(\n  values: number[] | undefined,\n): number[] {\n  if (!values || values.length === 0) return []\n  if (values.length === 1) return [values[0]]\n  if (values.length === 2) return createNumberRange(values)\n  return [values[0], values[1]]\n}\n\nexport function createDateFilterValue(\n  values: [Date, Date] | [Date] | [] | undefined,\n) {\n  if (!values || values.length === 0) return []\n  if (values.length === 1) return [values[0]]\n  if (values.length === 2) return createDateRange(values)\n  throw new Error('Cannot create date filter value from more than 2 values')\n}\n\nexport function createDateRange(values: [Date, Date]) {\n  const [a, b] = values\n  const [min, max] = isBefore(a, b) ? [a, b] : [b, a]\n\n  return [min, max]\n}\n\nexport function createNumberRange(values: number[] | undefined) {\n  let a = 0\n  let b = 0\n\n  if (!values || values.length === 0) return [a, b]\n  if (values.length === 1) {\n    a = values[0]\n  } else {\n    a = values[0]\n    b = values[1]\n  }\n\n  const [min, max] = a < b ? [a, b] : [b, a]\n\n  return [min, max]\n}\n\nexport function isColumnOption(value: unknown): value is ColumnOption {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    'value' in value &&\n    'label' in value\n  )\n}\n\nexport function isColumnOptionArray(value: unknown): value is ColumnOption[] {\n  return Array.isArray(value) && value.every(isColumnOption)\n}\n\nexport function isStringArray(value: unknown): value is string[] {\n  return Array.isArray(value) && value.every((v) => typeof v === 'string')\n}\n\nexport function isColumnOptionMap(\n  value: unknown,\n): value is Map<string, number> {\n  if (!(value instanceof Map)) {\n    return false\n  }\n  for (const key of value.keys()) {\n    if (typeof key !== 'string') {\n      return false\n    }\n  }\n  for (const val of value.values()) {\n    if (typeof val !== 'number') {\n      return false\n    }\n  }\n  return true\n}\n\nexport function isMinMaxTuple(value: unknown): value is [number, number] {\n  return (\n    Array.isArray(value) &&\n    value.length === 2 &&\n    typeof value[0] === 'number' &&\n    typeof value[1] === 'number'\n  )\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/lib/helpers.ts"
    },
    {
      "path": "registry/data-table-filter/ui/debounced-input.tsx",
      "content": "'use client'\n\nimport { Input } from '@/components/ui/input'\nimport { useCallback, useEffect, useState } from 'react'\nimport { debounce } from '../lib/debounce'\n\nexport function DebouncedInput({\n  value: initialValue,\n  onChange,\n  debounceMs = 500, // This is the wait time, not the function\n  ...props\n}: {\n  value: string | number\n  onChange: (value: string | number) => void\n  debounceMs?: number\n} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {\n  const [value, setValue] = useState(initialValue)\n\n  // Sync with initialValue when it changes\n  useEffect(() => {\n    setValue(initialValue)\n  }, [initialValue])\n\n  // Define the debounced function with useCallback\n  const debouncedOnChange = useCallback(\n    debounce((newValue: string | number) => {\n      onChange(newValue)\n    }, debounceMs), // Pass the wait time here\n    [debounceMs, onChange], // Dependencies\n  )\n\n  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newValue = e.target.value\n    setValue(newValue) // Update local state immediately\n    debouncedOnChange(newValue) // Call debounced version\n  }\n\n  return <Input {...props} value={value} onChange={handleChange} />\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/ui/debounced-input.tsx"
    },
    {
      "path": "registry/data-table-filter/lib/i18n.default.ts",
      "content": "import en from '../locales/en.json'\n\nexport type Locale = 'en'\n\ntype Translations = Record<string, string>\n\nconst translations: Record<Locale, Translations> = {\n  en,\n}\n\nexport function t(key: string, locale: Locale): string {\n  return translations[locale][key] ?? key\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/lib/i18n.ts"
    },
    {
      "path": "registry/data-table-filter/locales/en.json",
      "content": "{\n  \"clear\": \"Clear\",\n  \"search\": \"Search...\",\n  \"noresults\": \"No results.\",\n  \"operators\": \"Operators\",\n  \"filter\": \"Filter\",\n  \"and\": \"and\",\n  \"single\": \"Single\",\n  \"range\": \"Range\",\n  \"value\": \"Value\",\n  \"min\": \"Min\",\n  \"max\": \"Max\",\n  \"filters.option.is\": \"is\",\n  \"filters.option.isNot\": \"is not\",\n  \"filters.option.isAnyOf\": \"is any of\",\n  \"filters.option.isNoneOf\": \"is none of\",\n  \"filters.multiOption.include\": \"includes\",\n  \"filters.multiOption.exclude\": \"excludes\",\n  \"filters.multiOption.includeAnyOf\": \"includes any of\",\n  \"filters.multiOption.excludeAllOf\": \"excludes all of\",\n  \"filters.multiOption.includeAllOf\": \"includes all of\",\n  \"filters.multiOption.excludeIfAnyOf\": \"excludes if any of\",\n  \"filters.multiOption.excludeIfAll\": \"excludes if all of\",\n  \"filters.date.is\": \"is\",\n  \"filters.date.isNot\": \"is not\",\n  \"filters.date.isBefore\": \"is before\",\n  \"filters.date.isOnOrAfter\": \"is on or after\",\n  \"filters.date.isAfter\": \"is after\",\n  \"filters.date.isOnOrBefore\": \"is on or before\",\n  \"filters.date.isBetween\": \"is between\",\n  \"filters.date.isNotBetween\": \"is not between\",\n  \"filters.text.contains\": \"contains\",\n  \"filters.text.doesNotContain\": \"does not contain\",\n  \"filters.number.is\": \"is\",\n  \"filters.number.isNot\": \"is not\",\n  \"filters.number.greaterThan\": \"greater than\",\n  \"filters.number.greaterThanOrEqual\": \"greater than or equal\",\n  \"filters.number.lessThan\": \"less than\",\n  \"filters.number.lessThanOrEqual\": \"less than or equal\",\n  \"filters.number.isBetween\": \"is between\",\n  \"filters.number.isNotBetween\": \"is not between\"\n}\n",
      "type": "registry:file",
      "target": "components/data-table-filter/locales/en.json"
    }
  ]
}