{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "openlmis-combobox-filter",
  "title": "Combobox Filter",
  "description": "A searchable toolbar filter with caller-supplied options, search callbacks, status and empty messages.",
  "dependencies": [
    "@base-ui/react@^1",
    "lucide-react@^1"
  ],
  "registryDependencies": [
    "combobox",
    "input-group"
  ],
  "files": [
    {
      "path": "registry/components/openlmis/combobox-filter/combobox-filter.tsx",
      "content": "\"use client\"\n\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\"\nimport { XIcon } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport {\n  Combobox,\n  ComboboxContent,\n  ComboboxEmpty,\n  ComboboxInput,\n  ComboboxItem,\n  ComboboxList,\n} from \"@/components/ui/combobox\"\nimport { InputGroupAddon, InputGroupButton } from \"@/components/ui/input-group\"\n\nimport { narrowOptions } from \"@/components/openlmis/combobox-filter/narrow-options\"\n\nexport type ComboboxFilterOption = {\n  value: string\n  label: string\n  /** Muted beside the label, e.g. a facility's code. */\n  description?: string\n}\n\ntype ComboboxFilterProps = {\n  /** The placeholder and accessible name, e.g. \"Facility\". */\n  label: string\n  /** An empty string means no filter. */\n  value: string\n  onValueChange: (value: string) => void\n  options: ComboboxFilterOption[]\n  /** Most options rendered at once; past it a hint asks the user to search. */\n  limit?: number\n  /** Called with what the user types; the caller narrows `options` and they are listed as given. */\n  onSearch?: (text: string) => void\n  /** With `onSearch`, how many options match in all when `options` holds only some of them. */\n  total?: number\n  /** Shown when there is nothing to list. */\n  emptyMessage?: string\n  clearLabel?: string\n  /** Shown above the options while searching, such as how many matches were left out. */\n  status?: string\n  onOpenChange?: (open: boolean) => void\n}\n\n/** A toolbar filter for a long list: type to narrow it, pick one value, clear it. */\nexport function ComboboxFilter({\n  label,\n  value,\n  onValueChange,\n  options,\n  limit = 50,\n  onSearch,\n  total,\n  emptyMessage = \"No Matches\",\n  clearLabel = `Clear ${label}`,\n  status,\n  onOpenChange,\n}: ComboboxFilterProps) {\n  const [query, setQuery] = useState(\"\")\n  // Kept so the input still shows the pick once a search's results leave it out.\n  const [picked, setPicked] = useState<ComboboxFilterOption | null>(null)\n  const selected =\n    options.find((option) => option.value === value) ??\n    (picked?.value === value ? picked : null)\n  const { items, hint } = narrowOptions(options, {\n    query,\n    limit,\n    searched: Boolean(onSearch),\n    total: onSearch ? total : undefined,\n  })\n\n  return (\n    <Combobox\n      // Options are narrowed above, so Base UI does not filter them again.\n      filter={null}\n      isItemEqualToValue={(item, option) => item.value === option.value}\n      itemToStringLabel={(item) => item.label}\n      items={items}\n      onInputValueChange={(text, details) => {\n        // Only typing counts as a search, not the selected label filling the input.\n        const typed = details.reason === \"input-change\" ? text : \"\"\n        setQuery(typed)\n        onSearch?.(typed)\n      }}\n      onOpenChange={onOpenChange && ((open) => onOpenChange(open))}\n      onValueChange={(item) => {\n        setPicked(item)\n        onValueChange(item?.value ?? \"\")\n      }}\n      value={selected}\n    >\n      <ComboboxInput aria-label={label} className=\"w-full\" placeholder={label}>\n        {selected !== null && (\n          <InputGroupAddon align=\"inline-end\">\n            <ComboboxPrimitive.Clear\n              aria-label={clearLabel}\n              data-slot=\"combobox-clear\"\n              render={<InputGroupButton size=\"icon-xs\" variant=\"ghost\" />}\n            >\n              <XIcon />\n            </ComboboxPrimitive.Clear>\n          </InputGroupAddon>\n        )}\n      </ComboboxInput>\n      <ComboboxContent className=\"flex flex-col\">\n        <ComboboxPrimitive.Status className=\"text-xs text-muted-foreground\">\n          {onSearch && items.length > 0 && status && (\n            <span className=\"block px-3 pt-2\">{status}</span>\n          )}\n          {hint && <span className=\"block px-3 pt-2\">{hint}</span>}\n        </ComboboxPrimitive.Status>\n        <ComboboxEmpty>{emptyMessage}</ComboboxEmpty>\n        <ComboboxList>\n          {(option: ComboboxFilterOption) => (\n            <ComboboxItem key={option.value} value={option}>\n              <span className=\"min-w-0 truncate\" dir=\"auto\">\n                {option.label}\n              </span>\n              {option.description && (\n                <span\n                  className=\"ms-auto shrink-0 text-xs text-muted-foreground\"\n                  dir=\"auto\"\n                >\n                  {option.description}\n                </span>\n              )}\n            </ComboboxItem>\n          )}\n        </ComboboxList>\n      </ComboboxContent>\n    </Combobox>\n  )\n}",
      "type": "registry:component",
      "target": "components/openlmis/combobox-filter/combobox-filter.tsx"
    },
    {
      "path": "registry/components/openlmis/combobox-filter/narrow-options.ts",
      "content": "type NarrowableOption = { label: string; description?: string }\n\nconst searchText = (text: string) =>\n  text.normalize(\"NFD\").replace(/\\p{M}/gu, \"\").toLocaleLowerCase()\n\n// List the first `limit` matches, with a hint to search when more match than that.\nexport function narrowOptions<T extends NarrowableOption>(\n  options: readonly T[],\n  {\n    query,\n    limit,\n    searched = false,\n    total,\n  }: { query: string; limit: number; searched?: boolean; total?: number }\n): { items: readonly T[]; hint: string | null } {\n  const text = searchText(query.trim())\n  const matches =\n    searched || text === \"\"\n      ? options\n      : options.filter(\n          (option) =>\n            searchText(option.label).includes(text) ||\n            (option.description &&\n              searchText(option.description).includes(text))\n        )\n  const items = matches.slice(0, limit)\n  const count = total ?? matches.length\n  return {\n    items,\n    // An empty list shows the empty message alone.\n    hint:\n      count > limit && items.length > 0\n        ? `More than ${limit} entries. Search to narrow the list.`\n        : null,\n  }\n}",
      "type": "registry:lib",
      "target": "components/openlmis/combobox-filter/narrow-options.ts"
    }
  ],
  "cssVars": {
    "light": {
      "destructive": "oklch(0.577 0.245 27.325)",
      "destructive-foreground": "oklch(0.985 0 0)",
      "success": "oklch(0.596 0.145 163.225)",
      "success-foreground": "oklch(0.985 0 0)",
      "warning": "oklch(0.666 0.179 58.318)",
      "warning-foreground": "oklch(0.985 0 0)",
      "info": "oklch(0.55 0.13 237)",
      "info-foreground": "oklch(0.985 0 0)"
    },
    "dark": {
      "destructive": "oklch(0.704 0.191 22.216)",
      "destructive-foreground": "oklch(0.21 0.04 25)",
      "success": "oklch(0.696 0.17 162.48)",
      "success-foreground": "oklch(0.21 0.04 163)",
      "warning": "oklch(0.769 0.188 70.08)",
      "warning-foreground": "oklch(0.22 0.04 60)",
      "info": "oklch(0.7 0.14 237)",
      "info-foreground": "oklch(0.2 0.04 240)"
    }
  },
  "meta": {
    "project": "openlmis",
    "height": "352px"
  },
  "docs": "Requires a `base-*` style in components.json (the `shadcn init` default). Legacy `new-york`/`radix-*` styles install Radix primitives, which reject the `render` prop this item uses.",
  "categories": [
    "forms",
    "data-table"
  ],
  "type": "registry:component"
}
