{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "advance-select",
  "title": "Advance Select",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/advance-select/advance-select.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Check, ChevronDown, Loader2, X } from 'lucide-react'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from '@/components/ui/command'\nimport { Badge } from '@/components/ui/badge'\nimport { cn } from '@/lib/utils'\nimport { readKey } from './types'\nimport type { AdvanceSelectFieldNames } from './types'\n\nexport interface AdvanceSelectProps<T extends Record<string, unknown> | string | number> {\n  value?: unknown | unknown[]\n  onValueChange?: (value: unknown | unknown[], option: T | T[]) => void\n  options: T[]\n\n  // Mode\n  mode?: 'single' | 'multiple' | 'tags'\n\n  // Field mapping\n  fieldNames?: AdvanceSelectFieldNames\n\n  // Appearance\n  size?: 'sm' | 'default' | 'lg'\n  variant?: 'outlined' | 'filled' | 'borderless'\n  status?: 'default' | 'error' | 'warning'\n  placeholder?: string\n\n  // Search\n  showSearch?: boolean\n  searchValue?: string\n  onSearchChange?: (value: string) => void\n  autoClearSearchValue?: boolean\n  filterOption?: boolean | ((input: string, option: T) => boolean)\n  optionFilterProp?: string | string[]\n  filterSort?: (optionA: T, optionB: T, info: { searchValue: string }) => number\n\n  // Multiple/Tags\n  maxCount?: number\n  maxTagCount?: number\n  maxTagTextLength?: number\n  maxTagPlaceholder?: string | ((omittedValues: T[]) => string)\n  tokenSeparators?: string[]\n  hideSelected?: boolean\n  allowCreate?: boolean\n\n  // State\n  disabled?: boolean\n  loading?: boolean\n  allowClear?: boolean\n  open?: boolean\n  onOpenChange?: (open: boolean) => void\n  defaultOpen?: boolean\n  defaultActiveFirstOption?: boolean\n\n  // Customization\n  notFoundContent?: React.ReactNode\n  loadingText?: React.ReactNode\n  listHeight?: number\n  virtual?: boolean\n\n  className?: string\n\n  // Render slots\n  prefix?: React.ReactNode\n  suffix?: React.ReactNode\n  suffixIcon?: React.ReactNode\n  clearIcon?: React.ReactNode\n  renderLabel?: (info: { value: unknown; label: string }) => React.ReactNode\n  renderTag?: (info: {\n    value: unknown\n    label: string\n    closable: boolean\n    onClose: (e: React.MouseEvent | Event) => void\n  }) => React.ReactNode\n  renderOption?: (info: { option: T; index: number }) => React.ReactNode\n  emptyContent?: React.ReactNode\n\n  // Events\n  onSelect?: (value: unknown, option: T) => void\n  onDeselect?: (value: unknown, option: T) => void\n  onClear?: () => void\n  onFocus?: (event: React.FocusEvent) => void\n  onBlur?: (event: React.FocusEvent) => void\n  onPopupScroll?: (event: React.UIEvent) => void\n  onInputKeyDown?: (event: React.KeyboardEvent) => void\n}\n\nconst sizeClasses = {\n  sm: 'h-8 text-xs px-2.5 py-1',\n  default: 'h-9 text-sm px-3 py-1.5',\n  lg: 'h-11 text-base px-4 py-2',\n}\n\nconst variantClasses = {\n  outlined:\n    'border-input bg-transparent shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',\n  filled:\n    'border-transparent bg-muted/50 shadow-none focus-visible:bg-muted focus-visible:ring-ring/50 focus-visible:ring-[3px]',\n  borderless:\n    'border-transparent bg-transparent shadow-none focus-visible:bg-muted/30 focus-visible:ring-ring/50 focus-visible:ring-[3px]',\n}\n\nconst statusClasses = {\n  default: '',\n  error:\n    'border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 aria-invalid:border-destructive',\n  warning: 'border-[var(--warning)] focus-visible:border-[var(--warning)] focus-visible:ring-[var(--warning)]/20',\n}\n\nfunction AdvanceSelect<T extends Record<string, unknown> | string | number>({\n  value: modelValue,\n  onValueChange,\n  options,\n  mode = 'single',\n  fieldNames = {},\n  size = 'default',\n  variant = 'outlined',\n  status = 'default',\n  placeholder = 'Select...',\n  showSearch = false,\n  searchValue,\n  onSearchChange,\n  autoClearSearchValue = true,\n  filterOption = true,\n  optionFilterProp = 'label',\n  filterSort,\n  maxCount,\n  maxTagCount,\n  maxTagTextLength,\n  maxTagPlaceholder,\n  tokenSeparators = [],\n  hideSelected = false,\n  allowCreate = false,\n  disabled = false,\n  loading = false,\n  allowClear = true,\n  open,\n  onOpenChange,\n  defaultOpen = false,\n  defaultActiveFirstOption = true,\n  notFoundContent = 'No results.',\n  loadingText = 'Loading...',\n  listHeight = 300,\n  virtual = true,\n  className,\n  prefix,\n  suffix,\n  suffixIcon,\n  clearIcon,\n  renderLabel,\n  renderTag,\n  renderOption,\n  emptyContent,\n  onSelect,\n  onDeselect,\n  onClear,\n  onFocus,\n  onBlur,\n  onPopupScroll,\n  onInputKeyDown,\n}: AdvanceSelectProps<T>) {\n  const isOpenControlled = open !== undefined\n  const [internalOpen, setInternalOpen] = React.useState<boolean>(defaultOpen)\n  const isOpen = isOpenControlled ? open : internalOpen\n\n  function setOpen(v: boolean) {\n    if (!isOpenControlled) setInternalOpen(v)\n    onOpenChange?.(v)\n  }\n\n  const isQueryControlled = searchValue !== undefined\n  const [internalQuery, setInternalQuery] = React.useState('')\n  const query = isQueryControlled ? (searchValue as string) : internalQuery\n\n  function setQuery(v: string) {\n    if (!isQueryControlled) setInternalQuery(v)\n    onSearchChange?.(v)\n  }\n\n  const valueKey = fieldNames.value ?? 'value'\n  const labelKey = fieldNames.label ?? 'label'\n  const groupKey = fieldNames.group ?? 'group'\n  const disabledKey = fieldNames.disabled ?? 'disabled'\n\n  const getValue = React.useCallback((o: T): unknown => readKey(o, valueKey, o), [valueKey])\n  const getLabel = React.useCallback((o: T): string => String(readKey(o, labelKey, '')), [labelKey])\n  const getGroup = React.useCallback(\n    (o: T): string | undefined => {\n      const g = readKey(o, groupKey)\n      return g == null ? undefined : String(g)\n    },\n    [groupKey],\n  )\n  const isDisabledOption = React.useCallback((o: T): boolean => Boolean(readKey(o, disabledKey, false)), [disabledKey])\n\n  const isMultiple = mode === 'multiple' || mode === 'tags'\n\n  const selectedValues = React.useMemo<unknown[]>(() => {\n    if (modelValue == null) return []\n    if (isMultiple) {\n      return Array.isArray(modelValue) ? modelValue : []\n    }\n    return [modelValue]\n  }, [modelValue, isMultiple])\n\n  const selectedSet = React.useMemo(() => new Set(selectedValues), [selectedValues])\n\n  const selectedOptions = React.useMemo<T[]>(() => {\n    return selectedValues.map((v) => {\n      const found = options.find((o) => getValue(o) === v)\n      if (found) return found\n      // For created tags not in options, create a minimal option object\n      return { [labelKey]: String(v), [valueKey]: v } as T\n    })\n  }, [selectedValues, options, getValue, labelKey, valueKey])\n\n  function getOptionByValue(v: unknown): T | undefined {\n    return options.find((o) => getValue(o) === v)\n  }\n\n  function matchesFilter(o: T, q: string): boolean {\n    if (typeof filterOption === 'function') {\n      return filterOption(q, o)\n    }\n    if (filterOption === false) return true\n    const label = getLabel(o).toLowerCase()\n    const search = q.toLowerCase()\n    const propsToSearch = Array.isArray(optionFilterProp) ? optionFilterProp : [optionFilterProp]\n    for (const prop of propsToSearch) {\n      if (prop === 'label' && label.includes(search)) return true\n      const val = String(readKey(o, prop, '')).toLowerCase()\n      if (val.includes(search)) return true\n    }\n    return false\n  }\n\n  const filteredOptions = React.useMemo<T[]>(() => {\n    let result = options\n    const q = query.trim()\n\n    if (q) {\n      result = result.filter((o) => matchesFilter(o, q))\n    }\n\n    if (hideSelected && isMultiple) {\n      result = result.filter((o) => !selectedSet.has(getValue(o)))\n    }\n\n    if (q && filterSort) {\n      result = [...result].sort((a, b) => filterSort(a, b, { searchValue: q }))\n    }\n\n    return result\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [options, query, hideSelected, isMultiple, selectedSet, getValue, filterSort, filterOption, optionFilterProp])\n\n  const grouped = React.useMemo(() => {\n    const groups = new Map<string, T[]>()\n    for (const opt of filteredOptions) {\n      const key = getGroup(opt) ?? ''\n      if (!groups.has(key)) groups.set(key, [])\n      groups.get(key)!.push(opt)\n    }\n    return Array.from(groups, ([heading, items]) => ({ heading, items }))\n  }, [filteredOptions, getGroup])\n\n  const atMax = React.useMemo(() => {\n    if (typeof maxCount !== 'number') return false\n    const count = Array.isArray(modelValue) ? modelValue.length : modelValue ? 1 : 0\n    return count >= maxCount\n  }, [maxCount, modelValue])\n\n  const visibleTags = React.useMemo<T[]>(() => {\n    if (!isMultiple) return []\n    const opts = selectedOptions\n    if (typeof maxTagCount === 'number') {\n      return opts.slice(0, maxTagCount)\n    }\n    return opts\n  }, [isMultiple, selectedOptions, maxTagCount])\n\n  const hiddenTagCount = React.useMemo(() => {\n    if (!isMultiple) return 0\n    const opts = selectedOptions\n    if (typeof maxTagCount === 'number') {\n      return Math.max(0, opts.length - maxTagCount)\n    }\n    return 0\n  }, [isMultiple, selectedOptions, maxTagCount])\n\n  function displayLabel(o: T): string {\n    let label = getLabel(o)\n    if (maxTagTextLength && label.length > maxTagTextLength) {\n      label = label.slice(0, maxTagTextLength) + '...'\n    }\n    return label\n  }\n\n  function selectOption(option: T) {\n    if (isDisabledOption(option)) return\n    const v = getValue(option)\n\n    if (!isMultiple) {\n      onValueChange?.(v, option)\n      onSelect?.(v, option)\n      setOpen(false)\n      if (autoClearSearchValue) setQuery('')\n      return\n    }\n\n    const current = Array.isArray(modelValue) ? [...modelValue] : []\n    if (selectedSet.has(v)) {\n      const next = current.filter((x) => x !== v)\n      onValueChange?.(next, option)\n      onDeselect?.(v, option)\n    } else {\n      if (atMax) return\n      const next = [...current, v]\n      onValueChange?.(next, option)\n      onSelect?.(v, option)\n    }\n\n    if (autoClearSearchValue) setQuery('')\n  }\n\n  function removeTag(value: unknown, event: React.MouseEvent | Event) {\n    event.stopPropagation()\n    if (disabled) return\n    const current = Array.isArray(modelValue) ? [...modelValue] : []\n    const next = current.filter((x) => x !== value)\n    const option = getOptionByValue(value)\n    onValueChange?.(next, option as T)\n    if (option) onDeselect?.(value, option)\n  }\n\n  function clearAll(event?: React.MouseEvent | Event) {\n    event?.stopPropagation()\n    if (disabled) return\n    onClear?.()\n    if (isMultiple) {\n      onValueChange?.([], [])\n    } else {\n      onValueChange?.(null, undefined as unknown as T)\n    }\n    setQuery('')\n  }\n\n  function createTag() {\n    if (!allowCreate && mode !== 'tags') return\n    const q = query.trim()\n    if (!q) return\n    const exists = options.some((o) => getLabel(o) === q || String(getValue(o)) === q)\n    if (exists) return\n\n    if (!isMultiple) {\n      const newOption = { [labelKey]: q, [valueKey]: q } as T\n      onValueChange?.(q, newOption)\n      onSelect?.(q, newOption)\n      setOpen(false)\n      setQuery('')\n      return\n    }\n\n    if (atMax) return\n    const current = Array.isArray(modelValue) ? [...modelValue] : []\n    const next = [...current, q]\n    const newOption = { [labelKey]: q, [valueKey]: q } as T\n    onValueChange?.(next, newOption)\n    onSelect?.(q, newOption)\n    setQuery('')\n  }\n\n  function handleInputKeydown(event: React.KeyboardEvent) {\n    onInputKeyDown?.(event)\n    if (event.key === 'Enter' && query.trim() && mode === 'tags') {\n      event.preventDefault()\n      createTag()\n    }\n    if (tokenSeparators.length && mode === 'tags') {\n      if (tokenSeparators.includes(event.key)) {\n        event.preventDefault()\n        createTag()\n      }\n    }\n  }\n\n  // Clear query when the popover closes.\n  React.useEffect(() => {\n    if (!isOpen && autoClearSearchValue) setQuery('')\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isOpen])\n\n  const triggerBaseClasses = cn(\n    'flex w-full items-center justify-between gap-2 rounded-md border text-sm transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50',\n    sizeClasses[size],\n    variantClasses[variant],\n    statusClasses[status],\n    className,\n  )\n\n  const isEmpty = isMultiple\n    ? !Array.isArray(modelValue) || modelValue.length === 0\n    : modelValue == null || modelValue === ''\n\n  const showClear = allowClear && !isEmpty && !disabled && !loading\n  const showSearchInput = showSearch || mode === 'tags'\n\n  return (\n    <Popover open={isOpen} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <button\n          type=\"button\"\n          role=\"combobox\"\n          aria-expanded={isOpen}\n          disabled={disabled || loading}\n          data-uipkge=\"\"\n          data-slot=\"advance-select\"\n          className={triggerBaseClasses}\n          onFocus={onFocus}\n          onBlur={onBlur}\n        >\n          {/* Prefix */}\n          {prefix && <span className=\"shrink-0\">{prefix}</span>}\n\n          {/* Multiple mode tags */}\n          {isMultiple ? (\n            <div\n              className=\"flex flex-1 flex-nowrap items-center gap-1 overflow-x-auto\"\n              style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' } as React.CSSProperties}\n            >\n              {selectedOptions.length ? (\n                <>\n                  {visibleTags.map((opt) =>\n                    renderTag ? (\n                      <React.Fragment key={String(getValue(opt))}>\n                        {renderTag({\n                          value: getValue(opt),\n                          label: displayLabel(opt),\n                          closable: !disabled,\n                          onClose: (e) => removeTag(getValue(opt), e),\n                        })}\n                      </React.Fragment>\n                    ) : (\n                      <Badge\n                        key={String(getValue(opt))}\n                        variant=\"secondary\"\n                        className=\"bg-muted text-foreground h-6 gap-1 pr-1 pl-2 text-xs font-normal\"\n                      >\n                        <span className=\"truncate\">{displayLabel(opt)}</span>\n                        {!disabled && (\n                          <button\n                            type=\"button\"\n                            className=\"hover:bg-muted-foreground/20 rounded-full p-0.5 transition-colors\"\n                            aria-label={`Remove ${getLabel(opt)}`}\n                            onClick={(e) => removeTag(getValue(opt), e)}\n                          >\n                            <X className=\"size-3\" />\n                          </button>\n                        )}\n                      </Badge>\n                    ),\n                  )}\n                  {hiddenTagCount > 0 && (\n                    <Badge variant=\"secondary\" className=\"bg-muted text-foreground h-6 text-xs font-normal\">\n                      {typeof maxTagPlaceholder === 'function'\n                        ? maxTagPlaceholder(selectedOptions.slice(maxTagCount ?? 0))\n                        : maxTagPlaceholder\n                          ? maxTagPlaceholder\n                          : `+${hiddenTagCount}`}\n                    </Badge>\n                  )}\n                </>\n              ) : (\n                <span className=\"text-muted-foreground truncate\">{placeholder}</span>\n              )}\n            </div>\n          ) : (\n            /* Single mode display */\n            renderLabel ? (\n              renderLabel({\n                value: modelValue,\n                label: selectedOptions[0] ? getLabel(selectedOptions[0]) : '',\n              })\n            ) : (\n              <span\n                className={cn(\n                  'flex-1 truncate text-left',\n                  selectedOptions.length ? 'text-foreground' : 'text-muted-foreground',\n                )}\n              >\n                {selectedOptions[0] ? getLabel(selectedOptions[0]) : placeholder}\n              </span>\n            )\n          )}\n\n          {/* Suffix area */}\n          <span className=\"flex shrink-0 items-center gap-1\">\n            {suffix}\n\n            {loading ? (\n              <Loader2 className=\"text-muted-foreground size-4 animate-spin\" />\n            ) : showClear ? (\n              <button\n                type=\"button\"\n                className=\"text-muted-foreground hover:text-foreground rounded transition-colors\"\n                aria-label=\"Clear selection\"\n                onClick={clearAll}\n              >\n                {clearIcon ?? <X className=\"size-4\" aria-hidden=\"true\" />}\n              </button>\n            ) : (\n              suffixIcon ?? <ChevronDown className=\"text-muted-foreground size-4 opacity-50\" />\n            )}\n          </span>\n        </button>\n      </PopoverTrigger>\n\n      <PopoverContent\n        className=\"p-0\"\n        align=\"start\"\n        sideOffset={4}\n        style={{ width: 'var(--radix-popover-trigger-width)', maxHeight: `${listHeight}px` }}\n        onScroll={onPopupScroll}\n      >\n        <Command shouldFilter={false} className=\"flex flex-col overflow-hidden\">\n          {showSearchInput && (\n            <CommandInput\n              value={query}\n              onValueChange={setQuery}\n              placeholder={placeholder}\n              onKeyDown={handleInputKeydown}\n            />\n          )}\n\n          <CommandList className=\"flex-1 overflow-y-auto\">\n            {!loading && filteredOptions.length === 0 && (\n              <CommandEmpty>{emptyContent ?? notFoundContent}</CommandEmpty>\n            )}\n\n            {loading && filteredOptions.length === 0 && (\n              <div className=\"py-6 text-center text-sm\">{loadingText}</div>\n            )}\n\n            {grouped.map((group, gi) => (\n              <React.Fragment key={group.heading || gi}>\n                {gi > 0 && <CommandSeparator />}\n                <CommandGroup heading={group.heading || undefined}>\n                  {group.items.map((opt, idx) => (\n                    <CommandItem\n                      key={String(getValue(opt))}\n                      value={String(getValue(opt))}\n                      disabled={isDisabledOption(opt) || (atMax && !selectedSet.has(getValue(opt)))}\n                      data-active={gi === 0 && idx === 0 && defaultActiveFirstOption ? 'true' : undefined}\n                      style={\n                        virtual && options.length > 100\n                          ? ({ contentVisibility: 'auto' } as React.CSSProperties)\n                          : undefined\n                      }\n                      onSelect={() => selectOption(opt)}\n                    >\n                      <Check\n                        className={cn('mr-2 size-4 shrink-0', selectedSet.has(getValue(opt)) ? 'opacity-100' : 'opacity-0')}\n                      />\n                      {renderOption ? renderOption({ option: opt, index: idx }) : getLabel(opt)}\n                    </CommandItem>\n                  ))}\n                </CommandGroup>\n              </React.Fragment>\n            ))}\n\n            {/* Create new option in tags mode */}\n            {query.trim() && allowCreate && !options.some((o) => getLabel(o) === query.trim()) && (\n              <CommandItem value={`create:${query}`} onSelect={createTag}>\n                <Check className=\"mr-2 size-4 opacity-0\" />\n                Create &quot;{query.trim()}&quot;\n              </CommandItem>\n            )}\n          </CommandList>\n\n          {/* Footer for multiple mode */}\n          {isMultiple && selectedOptions.length > 0 && (\n            <div className=\"flex items-center justify-between border-t px-2 py-1.5 text-xs\">\n              <span className=\"text-muted-foreground\">{selectedOptions.length} selected</span>\n              <button type=\"button\" className=\"text-muted-foreground hover:text-foreground\" onClick={clearAll}>\n                Clear all\n              </button>\n            </div>\n          )}\n        </Command>\n      </PopoverContent>\n    </Popover>\n  )\n}\nAdvanceSelect.displayName = 'AdvanceSelect'\n\nexport { AdvanceSelect }\n",
      "type": "registry:ui",
      "target": "~/components/ui/advance-select/advance-select.tsx"
    },
    {
      "path": "packages/registry-react/components/advance-select/types.ts",
      "content": "/**\n * Shared option shape for `AdvanceSelect` and any future options-driven select.\n * Components accept this canonical shape OR any object shape if `fieldNames`\n * value/label keys are provided.\n */\nexport interface SelectOption<V = string> {\n  label: string\n  value: V\n  disabled?: boolean\n  /** Items sharing this key render under one heading. */\n  group?: string\n}\n\nexport interface AdvanceSelectFieldNames {\n  label?: string\n  value?: string\n  group?: string\n  disabled?: string\n}\n\n/**\n * Resolve `option[key]` with a default fallback. Used by AdvanceSelect so every\n * accessor obeys the configured value/label keys.\n */\nexport function readKey<T>(option: T, key: string, fallback?: unknown): unknown {\n  if (option == null || typeof option !== 'object') return fallback\n  const v = (option as Record<string, unknown>)[key]\n  return v === undefined ? fallback : v\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/advance-select/types.ts"
    },
    {
      "path": "packages/registry-react/components/advance-select/index.ts",
      "content": "export { AdvanceSelect, type AdvanceSelectProps } from './advance-select'\nexport { readKey, type AdvanceSelectFieldNames, type SelectOption } from './types'\n",
      "type": "registry:ui",
      "target": "~/components/ui/advance-select/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/popover.json",
    "https://uipkge.dev/r/react/command.json",
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/select.json"
  ],
  "description": "Searchable, async-capable select with keyboard navigation, multi-select, and option grouping. Drop in when the native `<select>` or the basic Select primitive runs out of room — large lists, debounced server-side filtering, custom rendered items.",
  "categories": [
    "form"
  ]
}