{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-filter-command",
  "title": "Data Table Filter Command Palette",
  "description": "Command palette for power-user filter input with history and keyboard shortcuts.",
  "dependencies": [
    "date-fns",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://data-table.openstatus.dev/r/data-table.json",
    "command",
    "kbd",
    "separator"
  ],
  "files": [
    {
      "path": "src/components/data-table/data-table-filter-command/index.tsx",
      "content": "\"use client\";\n\nimport { useDataTable } from \"@/components/data-table/data-table-provider\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"@/components/ui/command\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { useHotKey } from \"@/hooks/use-hot-key\";\nimport { useLocalStorage } from \"@/hooks/use-local-storage\";\nimport { getCommandHistoryKey } from \"@/lib/constants/local-storage\";\nimport { formatCompactNumber } from \"@/lib/format\";\nimport type { SchemaDefinition } from \"@/lib/store/schema/types\";\nimport { cn } from \"@/lib/utils\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { LoaderCircle, Search, X } from \"lucide-react\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { DataTableFilterField } from \"../types\";\nimport {\n  columnFiltersParserFromSchema,\n  getFieldOptions,\n  getFilterValue,\n  getWordByCaretPosition,\n  replaceInputByFieldType,\n} from \"./utils\";\n\ninterface DataTableFilterCommandProps {\n  // Schema definition for parsing/serializing filter values (BYOS)\n  schema: SchemaDefinition;\n  // Unique ID for this table (used to namespace localStorage)\n  tableId?: string;\n}\n\nexport function DataTableFilterCommand({\n  schema,\n  tableId = \"default\",\n}: DataTableFilterCommandProps) {\n  const {\n    table,\n    isLoading,\n    filterFields: _filterFields,\n    getFacetedUniqueValues,\n  } = useDataTable();\n  const columnFilters = table.getState().columnFilters;\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [open, setOpen] = useState<boolean>(false);\n  const [currentWord, setCurrentWord] = useState<string>(\"\");\n  // Guard to prevent effect cycle when serializing\n  const isSerializingRef = useRef(false);\n  const filterFields = useMemo(\n    () => _filterFields?.filter((i) => !i.commandDisabled),\n    [_filterFields],\n  );\n  const columnParser = useMemo(\n    () => columnFiltersParserFromSchema({ schema, filterFields }),\n    [schema, filterFields],\n  );\n  const [inputValue, setInputValue] = useState<string>(\n    columnParser.serialize(columnFilters),\n  );\n  const [lastSearches, setLastSearches] = useLocalStorage<\n    {\n      search: string;\n      timestamp: number;\n    }[]\n  >(getCommandHistoryKey(tableId), []);\n\n  useEffect(() => {\n    // Skip if this update came from serialization (prevents infinite loop)\n    if (isSerializingRef.current) {\n      isSerializingRef.current = false;\n      return;\n    }\n    // TODO: we could check for ARRAY_DELIMITER or SLIDER_DELIMITER to auto-set filter when typing\n    if (currentWord !== \"\" && open) return;\n    // reset\n    if (currentWord !== \"\" && !open) setCurrentWord(\"\");\n    // avoid recursion\n    if (inputValue.trim() === \"\" && !open) return;\n\n    const searchParams = columnParser.parse(inputValue);\n\n    const currentFilters = table.getState().columnFilters;\n    const currentEnabledFilters = currentFilters.filter((filter) => {\n      const field = _filterFields?.find((field) => field.value === filter.id);\n      return !field?.commandDisabled;\n    });\n    const currentDisabledFilters = currentFilters.filter((filter) => {\n      const field = _filterFields?.find((field) => field.value === filter.id);\n      return field?.commandDisabled;\n    });\n\n    const commandDisabledFilterKeys = currentDisabledFilters.reduce(\n      (prev, curr) => {\n        prev[curr.id] = curr.value;\n        return prev;\n      },\n      {} as Record<string, unknown>,\n    );\n\n    for (const key of Object.keys(searchParams)) {\n      const value = searchParams[key as keyof typeof searchParams];\n      table.getColumn(key)?.setFilterValue(value);\n    }\n    const currentFiltersToReset = currentEnabledFilters.filter((filter) => {\n      return !(filter.id in searchParams);\n    });\n    for (const filter of currentFiltersToReset) {\n      table.getColumn(filter.id)?.setFilterValue(undefined);\n    }\n\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [inputValue, open, currentWord]);\n\n  useEffect(() => {\n    // REMINDER: only update the input value if the command is closed (avoids jumps while open)\n    if (!open) {\n      // Set flag to prevent the parse effect from running after serialization\n      isSerializingRef.current = true;\n      setInputValue(columnParser.serialize(columnFilters));\n    }\n  }, [columnFilters, filterFields, open]);\n\n  useHotKey(() => setOpen((open) => !open), \"k\");\n\n  useEffect(() => {\n    if (open) {\n      inputRef?.current?.focus();\n    }\n  }, [open]);\n\n  return (\n    <div>\n      <button\n        type=\"button\"\n        className={cn(\n          \"group border-input bg-background text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground focus-within:border-ring focus-within:ring-ring/50 flex w-full items-center rounded-lg border px-3 transition-all outline-none focus-within:ring-[3px]\",\n          open ? \"hidden\" : \"visible\",\n        )}\n        onClick={() => setOpen(true)}\n      >\n        {isLoading ? (\n          <LoaderCircle className=\"text-muted-foreground group-hover:text-popover-foreground mr-2 h-4 w-4 shrink-0 animate-spin opacity-50\" />\n        ) : (\n          <Search className=\"text-muted-foreground group-hover:text-popover-foreground mr-2 h-4 w-4 shrink-0 opacity-50\" />\n        )}\n        <span className=\"h-11 w-full max-w-sm truncate py-3 text-left text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50 md:max-w-xl lg:max-w-4xl xl:max-w-5xl\">\n          {inputValue.trim() ? (\n            <span className=\"text-foreground\">{inputValue}</span>\n          ) : (\n            <span>Search data table...</span>\n          )}\n        </span>\n        <Kbd className=\"text-muted-foreground group-hover:text-accent-foreground ml-auto\">\n          <span className=\"mr-1\">⌘</span>\n          <span>K</span>\n        </Kbd>\n      </button>\n      <Command\n        className={cn(\n          \"border-border dark:bg-muted/50 overflow-visible rounded-lg border shadow-md [&>div]:border-none\",\n          open ? \"visible\" : \"hidden\",\n        )}\n        filter={(value, search, keywords) =>\n          getFilterValue({ value, search, keywords, currentWord })\n        }\n        // loop\n      >\n        <div\n          data-slot=\"command-input-wrapper\"\n          className=\"flex items-center gap-2 border-b px-3\"\n        >\n          <Search className=\"size-4 shrink-0 opacity-50\" />\n          <CommandPrimitive.Input\n            ref={inputRef}\n            value={inputValue}\n            onValueChange={setInputValue}\n            onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {\n              if (e.key === \"Escape\") inputRef?.current?.blur();\n            }}\n            onBlur={() => {\n              setOpen(false);\n              // FIXME: doesnt reflect the jumps\n              // FIXME: will save non-existing searches\n              // TODO: extract into function\n              const search = inputValue.trim();\n              if (!search) return;\n              const timestamp = Date.now();\n              const searchIndex = lastSearches.findIndex(\n                (item) => item.search === search,\n              );\n              if (searchIndex !== -1) {\n                lastSearches[searchIndex].timestamp = timestamp;\n                setLastSearches(lastSearches);\n                return;\n              }\n              setLastSearches([...lastSearches, { search, timestamp }]);\n              return;\n            }}\n            onInput={(e: React.FormEvent<HTMLInputElement>) => {\n              const caretPosition = e.currentTarget?.selectionStart || -1;\n              const value = e.currentTarget?.value || \"\";\n              const word = getWordByCaretPosition({ value, caretPosition });\n              setCurrentWord(word);\n            }}\n            placeholder=\"Search data table...\"\n            className=\"text-foreground placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\"\n          />\n        </div>\n        <div className=\"relative\">\n          <div className=\"border-border bg-popover text-popover-foreground animate-in absolute top-2 z-10 w-full overflow-hidden rounded-lg border shadow-md outline-hidden\">\n            {/* default height is 300px but in case of more, we'd like to tease the user */}\n            <CommandList className=\"max-h-[310px]\">\n              <CommandGroup heading=\"Filter\">\n                {filterFields.map((field) => {\n                  if (typeof field.value !== \"string\") return null;\n                  if (inputValue.includes(`${field.value}:`)) return null;\n                  // TBD: should we handle this in the component?\n                  return (\n                    <CommandItem\n                      key={field.value}\n                      value={field.value}\n                      onMouseDown={(e) => {\n                        e.preventDefault();\n                        e.stopPropagation();\n                      }}\n                      onSelect={(value) => {\n                        setInputValue((prev) => {\n                          if (currentWord.trim() === \"\") {\n                            const input = `${prev}${value}`;\n                            return `${input}:`;\n                          }\n                          // lots of cheat\n                          const isStarting = currentWord === prev;\n                          const prefix = isStarting ? \"\" : \" \";\n                          const input = prev.replace(\n                            `${prefix}${currentWord}`,\n                            `${prefix}${value}`,\n                          );\n                          return `${input}:`;\n                        });\n                        setCurrentWord(`${value}:`);\n                      }}\n                      className=\"group\"\n                    >\n                      {field.value}\n                      <CommandItemSuggestions field={field} />\n                    </CommandItem>\n                  );\n                })}\n              </CommandGroup>\n              <CommandSeparator />\n              <CommandGroup heading=\"Query\">\n                {filterFields?.map((field) => {\n                  if (typeof field.value !== \"string\") return null;\n                  if (!currentWord.includes(`${field.value}:`)) return null;\n\n                  const column = table.getColumn(field.value);\n                  const facetedValue =\n                    getFacetedUniqueValues?.(table, field.value) ||\n                    column?.getFacetedUniqueValues();\n\n                  const options = getFieldOptions({ field, facetedValue });\n\n                  return options.map((optionValue) => {\n                    return (\n                      <CommandItem\n                        key={`${String(field.value)}:${optionValue}`}\n                        value={`${String(field.value)}:${optionValue}`}\n                        onMouseDown={(e) => {\n                          e.preventDefault();\n                          e.stopPropagation();\n                        }}\n                        onSelect={(value) => {\n                          setInputValue((prev) =>\n                            replaceInputByFieldType({\n                              prev,\n                              currentWord,\n                              optionValue,\n                              value,\n                              field,\n                            }),\n                          );\n                          setCurrentWord(\"\");\n                        }}\n                      >\n                        {`${optionValue}`}\n                        {facetedValue?.has(optionValue) ? (\n                          <span className=\"text-muted-foreground ml-auto font-mono\">\n                            {formatCompactNumber(\n                              facetedValue.get(optionValue) || 0,\n                            )}\n                          </span>\n                        ) : null}\n                      </CommandItem>\n                    );\n                  });\n                })}\n              </CommandGroup>\n              <CommandSeparator />\n              <CommandGroup heading=\"Suggestions\">\n                {lastSearches\n                  ?.sort((a, b) => b.timestamp - a.timestamp)\n                  .slice(0, 5)\n                  .map((item) => {\n                    return (\n                      <CommandItem\n                        key={`suggestion:${item.search}`}\n                        value={`suggestion:${item.search}`}\n                        onMouseDown={(e) => {\n                          e.preventDefault();\n                          e.stopPropagation();\n                        }}\n                        onSelect={(value) => {\n                          const search = value.replace(\"suggestion:\", \"\");\n                          setInputValue(`${search} `);\n                          setCurrentWord(\"\");\n                        }}\n                        className=\"group\"\n                      >\n                        {item.search}\n                        <span className=\"text-muted-foreground/80 ml-auto truncate group-aria-selected:block\">\n                          {formatDistanceToNow(item.timestamp, {\n                            addSuffix: true,\n                          })}\n                        </span>\n                        <button\n                          type=\"button\"\n                          onMouseDown={(e) => {\n                            e.preventDefault();\n                            e.stopPropagation();\n                          }}\n                          onClick={(e) => {\n                            e.preventDefault();\n                            e.stopPropagation();\n                            // TODO: extract into function\n                            setLastSearches(\n                              lastSearches.filter(\n                                (i) => i.search !== item.search,\n                              ),\n                            );\n                          }}\n                          className=\"hover:bg-background ml-1 hidden rounded-md p-0.5 group-aria-selected:block\"\n                        >\n                          <X className=\"h-4 w-4\" />\n                        </button>\n                      </CommandItem>\n                    );\n                  })}\n              </CommandGroup>\n              <CommandEmpty>No results found.</CommandEmpty>\n            </CommandList>\n            <div\n              className=\"bg-accent/50 text-accent-foreground flex flex-wrap justify-between gap-3 border-t px-2 py-1.5 text-sm\"\n              cmdk-footer=\"\"\n            >\n              <div className=\"flex flex-wrap gap-3\">\n                <span>\n                  Use <Kbd>↑</Kbd> <Kbd>↓</Kbd> to navigate\n                </span>\n                <span>\n                  <Kbd>Enter</Kbd> to query\n                </span>\n                <span>\n                  <Kbd>Esc</Kbd> to close\n                </span>\n                <Separator orientation=\"vertical\" className=\"my-auto h-3\" />\n                <span>\n                  Union: <Kbd>regions:a,b</Kbd>\n                </span>\n                <span>\n                  Range: <Kbd>p95:59-340</Kbd>\n                </span>\n                <span>\n                  Spaces: <Kbd>name:&quot;a b&quot;</Kbd>\n                </span>\n              </div>\n              {lastSearches.length ? (\n                <button\n                  type=\"button\"\n                  className=\"text-muted-foreground hover:text-accent-foreground\"\n                  onMouseDown={(e) => {\n                    e.preventDefault();\n                    e.stopPropagation();\n                  }}\n                  onClick={() => setLastSearches([])}\n                >\n                  Clear suggestions\n                </button>\n              ) : null}\n            </div>\n          </div>\n        </div>\n      </Command>\n    </div>\n  );\n}\n\n// function CommandItemType<TData>\n\nfunction CommandItemSuggestions<TData>({\n  field,\n}: {\n  field: DataTableFilterField<TData>;\n}) {\n  const { table, getFacetedMinMaxValues, getFacetedUniqueValues } =\n    useDataTable();\n  const value = field.value as string;\n  switch (field.type) {\n    case \"checkbox\": {\n      return (\n        <span className=\"text-muted-foreground/80 ml-1 hidden truncate group-aria-selected:block\">\n          {getFacetedUniqueValues\n            ? Array.from(getFacetedUniqueValues(table, value)?.keys() || [])\n                .map((value) => `[${value}]`)\n                .join(\" \")\n            : field.options?.map(({ value }) => `[${value}]`).join(\" \")}\n        </span>\n      );\n    }\n    case \"slider\": {\n      const [min, max] = getFacetedMinMaxValues?.(table, value) || [\n        field.min,\n        field.max,\n      ];\n      return (\n        <span className=\"text-muted-foreground/80 ml-1 hidden truncate group-aria-selected:block\">\n          [{min} - {max}]\n        </span>\n      );\n    }\n    case \"input\": {\n      return (\n        <span className=\"text-muted-foreground/80 ml-1 hidden truncate group-aria-selected:block\">\n          [{`${String(field.value)}`} input]\n        </span>\n      );\n    }\n    default: {\n      return null;\n    }\n  }\n}\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/data-table/data-table-filter-command/utils.ts",
      "content": "import {\n  ARRAY_DELIMITER,\n  RANGE_DELIMITER,\n  SLIDER_DELIMITER,\n} from \"@/lib/delimiters\";\nimport { isArrayOfDates } from \"@/lib/is-array\";\nimport type {\n  FieldBuilder,\n  SchemaDefinition,\n} from \"@/lib/store/schema/types\";\nimport type { ColumnFiltersState } from \"@tanstack/react-table\";\nimport type { DataTableFilterField } from \"../types\";\n\n/**\n * Extracts the word from the given string at the specified caret position.\n */\nexport function getWordByCaretPosition({\n  value,\n  caretPosition,\n}: {\n  value: string;\n  caretPosition: number;\n}) {\n  let start = caretPosition;\n  let end = caretPosition;\n\n  while (start > 0 && value[start - 1] !== \" \") start--;\n  while (end < value.length && value[end] !== \" \") end++;\n\n  const word = value.substring(start, end);\n  return word;\n}\n\n/**\n * Quote a value if it contains spaces\n */\nfunction quoteIfNeeded(val: string | number | boolean | undefined): string {\n  const str = `${val}`;\n  if (str.includes(\" \")) {\n    return `\"${str}\"`;\n  }\n  return str;\n}\n\nexport function replaceInputByFieldType<TData>({\n  prev,\n  currentWord,\n  optionValue,\n  value,\n  field,\n}: {\n  prev: string;\n  currentWord: string;\n  optionValue?: string | number | boolean | undefined; // FIXME: use DataTableFilterField<TData>[\"options\"][number];\n  value: string;\n  field: DataTableFilterField<TData>;\n}) {\n  switch (field.type) {\n    case \"checkbox\": {\n      if (currentWord.includes(ARRAY_DELIMITER)) {\n        const words = currentWord.split(ARRAY_DELIMITER);\n        words[words.length - 1] = quoteIfNeeded(optionValue);\n        const input = prev.replace(currentWord, words.join(ARRAY_DELIMITER));\n        return `${input.trim()} `;\n      }\n      break;\n    }\n    case \"slider\": {\n      if (currentWord.includes(SLIDER_DELIMITER)) {\n        const words = currentWord.split(SLIDER_DELIMITER);\n        words[words.length - 1] = `${optionValue}`;\n        const input = prev.replace(currentWord, words.join(SLIDER_DELIMITER));\n        return `${input.trim()} `;\n      }\n      break;\n    }\n    case \"timerange\": {\n      if (currentWord.includes(RANGE_DELIMITER)) {\n        const words = currentWord.split(RANGE_DELIMITER);\n        words[words.length - 1] = `${optionValue}`;\n        const input = prev.replace(currentWord, words.join(RANGE_DELIMITER));\n        return `${input.trim()} `;\n      }\n      break;\n    }\n  }\n\n  // Default: set a fresh filter value, quoting if it contains spaces\n  const quotedValue = quoteIfNeeded(optionValue) || value;\n  const input = prev.replace(\n    currentWord,\n    `${String(field.value)}:${quotedValue}`,\n  );\n  return `${input.trim()} `;\n}\n\nexport function getFieldOptions<TData>({\n  field,\n  facetedValue,\n}: {\n  field: DataTableFilterField<TData>;\n  facetedValue?: Map<unknown, number>;\n}) {\n  switch (field.type) {\n    case \"slider\": {\n      if (field.options?.length) {\n        return field.options\n          .map(({ value }) => value)\n          .sort((a, b) => Number(a) - Number(b))\n          .filter(notEmpty);\n      }\n      // Use only the values that actually exist in the data to avoid\n      // generating thousands of intermediate integers (e.g. salary 58k-155k).\n      if (facetedValue?.size) {\n        return Array.from(facetedValue.keys())\n          .map(Number)\n          .filter((n) => !isNaN(n))\n          .sort((a, b) => a - b);\n      }\n      return [];\n    }\n    default: {\n      return field.options?.map(({ value }) => value).filter(notEmpty) || [];\n    }\n  }\n}\n\nexport function getFilterValue({\n  value,\n  search,\n  currentWord,\n}: {\n  value: string;\n  search: string;\n  keywords?: string[] | undefined;\n  currentWord: string;\n}): number {\n  /**\n   * @example value \"suggestion:public:true regions,ams,gru,fra\"\n   */\n  if (value.startsWith(\"suggestion:\")) {\n    const rawValue = value.toLowerCase().replace(\"suggestion:\", \"\");\n    if (rawValue.includes(search)) return 1;\n    return 0;\n  }\n\n  /** */\n  if (value.toLowerCase().includes(currentWord.toLowerCase())) return 1;\n\n  /**\n   * @example checkbox [filter, query] = [\"regions\", \"ams,gru,fra\"]\n   * @example slider [filter, query] = [\"p95\", \"0-3000\"]\n   * @example input [filter, query] = [\"name\", \"api\"]\n   */\n  const [filter, query] = currentWord.toLowerCase().split(\":\");\n  if (query && value.startsWith(`${filter}:`)) {\n    if (query.includes(ARRAY_DELIMITER)) {\n      /**\n       * array of n elements\n       * @example queries = [\"ams\", \"gru\", \"fra\"]\n       */\n      const queries = query.split(ARRAY_DELIMITER);\n      const rawValue = value.toLowerCase().replace(`${filter}:`, \"\");\n      if (\n        queries.some((item, i) => item === rawValue && i !== queries.length - 1)\n      )\n        return 0;\n      if (queries.some((item) => rawValue.includes(item))) return 1;\n    }\n    if (query.includes(SLIDER_DELIMITER)) {\n      /**\n       * range between 2 elements\n       * @example queries = [\"0\", \"3000\"]\n       */\n      const queries = query.split(SLIDER_DELIMITER);\n      const rawValue = value.toLowerCase().replace(`${filter}:`, \"\");\n\n      const rawValueAsNumber = Number.parseInt(rawValue);\n      const queryAsNumber = Number.parseInt(queries[0]);\n\n      if (queryAsNumber < rawValueAsNumber) {\n        if (rawValue.includes(queries[1])) return 1;\n        return 0;\n      }\n      return 0;\n    }\n    const rawValue = value.toLowerCase().replace(`${filter}:`, \"\");\n    if (rawValue.includes(query)) return 1;\n  }\n  return 0;\n}\n\nexport function getFieldValueByType<TData>({\n  field,\n  value,\n}: {\n  field?: DataTableFilterField<TData>;\n  value: unknown;\n}) {\n  if (!field) return null;\n\n  switch (field.type) {\n    case \"slider\": {\n      if (Array.isArray(value)) {\n        return value.join(SLIDER_DELIMITER);\n      }\n      return value;\n    }\n    case \"checkbox\": {\n      if (Array.isArray(value)) {\n        return value.join(ARRAY_DELIMITER);\n      }\n      // REMINER: inversed logic\n      if (typeof value === \"string\") {\n        return value.split(ARRAY_DELIMITER);\n      }\n      return value;\n    }\n    case \"timerange\": {\n      if (Array.isArray(value)) {\n        if (isArrayOfDates(value)) {\n          return value.map((date) => date.getTime()).join(RANGE_DELIMITER);\n        }\n        return value.join(RANGE_DELIMITER);\n      }\n      if (value instanceof Date) {\n        return value.getTime();\n      }\n      return value;\n    }\n    default: {\n      return value;\n    }\n  }\n}\n\nexport function notEmpty<TValue>(\n  value: TValue | null | undefined,\n): value is TValue {\n  return value !== null && value !== undefined;\n}\n\n/**\n * Tokenize input string, respecting quoted values\n *\n * Examples:\n * - `name:john regions:ams` → [[\"name\", \"john\"], [\"regions\", \"ams\"]]\n * - `name:\"john doe\" regions:ams` → [[\"name\", \"john doe\"], [\"regions\", \"ams\"]]\n * - `url:\"https://example.com/path with spaces\"` → [[\"url\", \"https://example.com/path with spaces\"]]\n */\nexport function tokenizeFilterInput(input: string): Array<[string, string]> {\n  const results: Array<[string, string]> = [];\n  const trimmed = input.trim();\n\n  // Regex to match: key:\"quoted value\" or key:unquoted_value\n  // This handles:\n  // - key:\"value with spaces\"\n  // - key:'value with spaces' (single quotes)\n  // - key:valueWithoutSpaces\n  const regex = /(\\w+):(?:\"([^\"]*)\"|'([^']*)'|(\\S+))/g;\n\n  let match;\n  while ((match = regex.exec(trimmed)) !== null) {\n    const key = match[1];\n    // Value is in group 2 (double quotes), group 3 (single quotes), or group 4 (unquoted)\n    const value = match[2] ?? match[3] ?? match[4];\n    if (key && value !== undefined) {\n      results.push([key, value]);\n    }\n  }\n\n  return results;\n}\n\n/**\n * Serialize a value, adding quotes if it contains spaces\n */\nexport function serializeFilterValue(value: string): string {\n  if (value.includes(\" \")) {\n    return `\"${value}\"`;\n  }\n  return value;\n}\n\n/**\n * Schema-based column filters parser for BYOS\n *\n * This parser works with the new schema system instead of nuqs ParserBuilder.\n */\nexport function columnFiltersParserFromSchema<TData>({\n  schema,\n  filterFields,\n}: {\n  schema: SchemaDefinition;\n  filterFields: DataTableFilterField<TData>[];\n}) {\n  return {\n    parse: (inputValue: string) => {\n      // Use tokenizer that respects quoted values\n      const tokens = tokenizeFilterInput(inputValue);\n      const values = tokens.reduce(\n        (prev, [name, value]) => {\n          prev[name] = value;\n          return prev;\n        },\n        {} as Record<string, string>,\n      );\n\n      const searchParams = Object.entries(values).reduce(\n        (prev, [key, value]) => {\n          const fieldBuilder = schema[key] as FieldBuilder<unknown> | undefined;\n          if (!fieldBuilder) return prev;\n\n          let parsed = fieldBuilder._config.parse(value);\n          if (parsed !== null) {\n            // Slider fields expect [min, max] for inNumberRange — if a single\n            // value is provided (e.g. \"amount:1800\"), duplicate it so the range\n            // becomes [1800, 1800] (exact match).\n            const field = filterFields?.find((f) => f.value === key);\n            if (\n              field?.type === \"slider\" &&\n              Array.isArray(parsed) &&\n              parsed.length === 1\n            ) {\n              parsed = [parsed[0], parsed[0]];\n            }\n            prev[key] = parsed;\n          }\n          return prev;\n        },\n        {} as Record<string, unknown>,\n      );\n\n      return searchParams;\n    },\n    serialize: (columnFilters: ColumnFiltersState) => {\n      const values = columnFilters.reduce((prev, curr) => {\n        const { commandDisabled } = filterFields?.find(\n          (field) => curr.id === field.value,\n        ) || { commandDisabled: true };\n        const fieldBuilder = schema[curr.id] as\n          | FieldBuilder<unknown>\n          | undefined;\n\n        if (commandDisabled || !fieldBuilder) return prev;\n\n        const serialized = fieldBuilder._config.serialize(curr.value);\n        if (!serialized) return prev;\n\n        // Wrap in quotes if value contains spaces\n        const quotedValue = serializeFilterValue(serialized);\n        return `${prev}${curr.id}:${quotedValue} `;\n      }, \"\");\n\n      return values;\n    },\n  };\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:block"
}