{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "advance-select",
  "title": "Advance Select",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/advance-select/advance-select.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  Directive,\n  ElementRef,\n  EmbeddedViewRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  OnInit,\n  Output,\n  SimpleChanges,\n  TemplateRef,\n  ViewChild,\n  ViewContainerRef,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport {\n  UiPopoverComponent,\n  UiPopoverContentComponent,\n  UiPopoverTriggerComponent,\n} from '@/ui/popover/popover.component'\nimport {\n  UiCommandComponent,\n  UiCommandEmptyComponent,\n  UiCommandGroupComponent,\n  UiCommandInputComponent,\n  UiCommandItemComponent,\n  UiCommandListComponent,\n  UiCommandSeparatorComponent,\n} from '@/ui/command/command.component'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { readKey, type AdvanceSelectFieldNames } from './types'\n\nexport type { AdvanceSelectFieldNames, SelectOption } from './types'\nexport type AdvanceSelectMode = 'single' | 'multiple' | 'tags'\nexport type AdvanceSelectSize = 'sm' | 'default' | 'lg'\nexport type AdvanceSelectVariant = 'outlined' | 'filled' | 'borderless'\nexport type AdvanceSelectStatus = 'default' | 'error' | 'warning'\n/** Any option shape: `SelectOption`, a custom object mapped through `fieldNames`, or a primitive. */\nexport type AdvanceSelectOption = Record<string, unknown> | string | number\n/** Rich content slot (React `ReactNode`): plain text or an `<ng-template>`. */\nexport type AdvanceSelectContent = string | TemplateRef<unknown> | null | undefined\n\n/** `renderLabel` template context (React `renderLabel(info)`). */\nexport interface AdvanceSelectLabelContext {\n  value: unknown\n  label: string\n}\n/** `renderTag` template context (React `renderTag(info)`). */\nexport interface AdvanceSelectTagContext {\n  value: unknown\n  label: string\n  closable: boolean\n  onClose: (e?: Event) => void\n}\n/** `renderOption` template context (React `renderOption(info)`). */\nexport interface AdvanceSelectOptionContext<T = AdvanceSelectOption> {\n  option: T\n  index: number\n}\n\nconst sizeClasses: Record<AdvanceSelectSize, string> = {\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: Record<AdvanceSelectVariant, string> = {\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: Record<AdvanceSelectStatus, string> = {\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-warning focus-visible:border-warning focus-visible:ring-warning/20',\n}\n\n/**\n * Template outlet with a context (`<ng-container [uiAdvanceSelectOutlet]=\"tpl\" [uiAdvanceSelectOutletContext]=\"ctx\" />`).\n * The context is exposed both as `$implicit` and spread, so `let-info` and `let-label=\"label\"` both work.\n */\n@Directive({ selector: '[uiAdvanceSelectOutlet]', standalone: true })\nexport class UiAdvanceSelectOutletDirective implements OnChanges, OnDestroy {\n  @Input('uiAdvanceSelectOutlet') template: TemplateRef<unknown> | null | undefined\n  @Input('uiAdvanceSelectOutletContext') context: object | undefined\n  private readonly vcr = inject(ViewContainerRef)\n  private view: EmbeddedViewRef<Record<string, unknown>> | null = null\n\n  ngOnChanges(changes: SimpleChanges): void {\n    const ctx = { $implicit: this.context, ...(this.context ?? {}) }\n    if (changes['template'] || !this.view) {\n      this.vcr.clear()\n      this.view = this.template\n        ? (this.vcr.createEmbeddedView(this.template, ctx) as EmbeddedViewRef<Record<string, unknown>>)\n        : null\n    } else {\n      Object.assign(this.view.context, ctx)\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.vcr.clear()\n  }\n}\n\n/**\n * Sits on the element projected into PopoverContent and styles the portalled panel the way\n * React passes `style` / `onScroll` to PopoverContent (width tracks the trigger, max height).\n */\n@Directive({ selector: '[uiAdvanceSelectPanel]', standalone: true })\nexport class UiAdvanceSelectPanelDirective implements OnInit, OnChanges, OnDestroy {\n  @Input() listHeight = 300\n  @Output() popupScroll = new EventEmitter<Event>()\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  private panel: HTMLElement | null = null\n  private readonly onScroll = (e: Event) => this.popupScroll.emit(e)\n\n  ngOnInit(): void {\n    this.panel = this.el.parentElement\n    this.panel?.addEventListener('scroll', this.onScroll)\n    this.apply()\n  }\n\n  ngOnChanges(): void {\n    this.apply()\n  }\n\n  private apply(): void {\n    if (!this.panel) return\n    this.panel.style.width = 'var(--radix-popover-trigger-width)'\n    this.panel.style.maxHeight = `${this.listHeight}px`\n  }\n\n  ngOnDestroy(): void {\n    this.panel?.removeEventListener('scroll', this.onScroll)\n  }\n}\n\n/**\n * Angular port of UIPKGE AdvanceSelect, 1:1 with the React component: a Popover whose\n * trigger is the combobox button (tags as Badges in multiple / tags mode, clear X, spinner),\n * and whose content is a Command list (search input, grouped items with separators, check\n * marks, \"Create\" item, \"N selected / Clear all\" footer). Selection is controlled through\n * `value` / `valueChange` exactly like React's `value` / `onValueChange`; `open` and\n * `searchValue` are controlled-or-uncontrolled. Class strings copied verbatim from React.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-advance-select, [ui-advance-select]',\n  standalone: true,\n  imports: [\n    UiPopoverComponent,\n    UiPopoverTriggerComponent,\n    UiPopoverContentComponent,\n    UiCommandComponent,\n    UiCommandInputComponent,\n    UiCommandListComponent,\n    UiCommandEmptyComponent,\n    UiCommandGroupComponent,\n    UiCommandItemComponent,\n    UiCommandSeparatorComponent,\n    UiBadgeComponent,\n    UiAdvanceSelectOutletDirective,\n    UiAdvanceSelectPanelDirective,\n  ],\n  // React renders no wrapper element. The host is a plain block (not `contents`) so parent\n  // spacing utilities (space-y-*) still reach it like they reach React's trigger button.\n  host: { '[class]': '\"block\"' },\n  template: `\n    <ui-popover [open]=\"isOpen\" (openChange)=\"setOpen($event)\">\n      <button\n        #triggerEl\n        ui-popover-trigger\n        type=\"button\"\n        role=\"combobox\"\n        [attr.aria-expanded]=\"isOpen\"\n        [attr.aria-invalid]=\"status === 'error' ? true : null\"\n        [disabled]=\"disabled || loading\"\n        data-uipkge=\"\"\n        data-slot=\"advance-select\"\n        [class]=\"triggerClass\"\n        (focus)=\"focus.emit($event)\"\n        (blur)=\"blur.emit($event)\"\n      >\n        @if (prefix) {\n          <span class=\"shrink-0\"><ng-container [uiAdvanceSelectOutlet]=\"prefix\" /></span>\n        }\n        @if (isMultiple) {\n          <div\n            class=\"flex flex-1 [scrollbar-width:none] flex-nowrap items-center gap-1 overflow-x-auto [-ms-overflow-style:none]\"\n          >\n            @if (selectedOptions.length) {\n              @for (opt of visibleTags; track tagKey(opt)) {\n                @if (renderTag) {\n                  <ng-container [uiAdvanceSelectOutlet]=\"renderTag\" [uiAdvanceSelectOutletContext]=\"tagContext(opt)\" />\n                } @else {\n                  <span\n                    ui-badge\n                    variant=\"secondary\"\n                    class=\"bg-muted text-foreground h-6 gap-1 pr-1 pl-2 text-xs font-normal\"\n                  >\n                    <span class=\"truncate\">{{ displayLabel(opt) }}</span>\n                    @if (!disabled) {\n                      <span\n                        role=\"button\"\n                        tabindex=\"0\"\n                        class=\"hover:bg-muted-foreground/20 inline-flex items-center justify-center rounded-full p-0.5 transition-colors\"\n                        [attr.aria-label]=\"'Remove ' + getLabel(opt)\"\n                        (click)=\"removeTag(getValue(opt), $event)\"\n                        (keydown)=\"onActionKeydown($event, removeTagFn(opt))\"\n                      >\n                        <svg\n                          xmlns=\"http://www.w3.org/2000/svg\"\n                          width=\"24\"\n                          height=\"24\"\n                          viewBox=\"0 0 24 24\"\n                          fill=\"none\"\n                          stroke=\"currentColor\"\n                          stroke-width=\"2\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          class=\"lucide lucide-x size-3\"\n                          aria-hidden=\"true\"\n                        >\n                          <path d=\"M18 6 6 18\" />\n                          <path d=\"m6 6 12 12\" />\n                        </svg>\n                      </span>\n                    }\n                  </span>\n                }\n              }\n              @if (hiddenTagCount > 0) {\n                <span ui-badge variant=\"secondary\" class=\"bg-muted text-foreground h-6 text-xs font-normal\">{{\n                  maxTagPlaceholderText\n                }}</span>\n              }\n            } @else {\n              <span class=\"text-muted-foreground truncate\">{{ placeholder }}</span>\n            }\n          </div>\n        } @else if (renderLabel) {\n          <ng-container [uiAdvanceSelectOutlet]=\"renderLabel\" [uiAdvanceSelectOutletContext]=\"labelContext\" />\n        } @else {\n          <span [class]=\"singleLabelClass\">{{ selectedOptions[0] ? getLabel(selectedOptions[0]) : placeholder }}</span>\n        }\n\n        <span class=\"flex shrink-0 items-center gap-1\">\n          @if (suffix) {\n            <ng-container [uiAdvanceSelectOutlet]=\"suffix\" />\n          }\n          @if (loading) {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"lucide lucide-loader-circle text-muted-foreground size-4 animate-spin\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n            </svg>\n          } @else if (showClear) {\n            <span\n              role=\"button\"\n              tabindex=\"0\"\n              class=\"text-muted-foreground hover:text-foreground inline-flex cursor-pointer items-center rounded transition-colors\"\n              aria-label=\"Clear selection\"\n              (click)=\"clearAll($event)\"\n              (keydown)=\"onActionKeydown($event, clearAllFn)\"\n            >\n              @if (clearIcon) {\n                <ng-container [uiAdvanceSelectOutlet]=\"clearIcon\" />\n              } @else {\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"lucide lucide-x size-4\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"M18 6 6 18\" />\n                  <path d=\"m6 6 12 12\" />\n                </svg>\n              }\n            </span>\n          } @else if (suffixIcon) {\n            <ng-container [uiAdvanceSelectOutlet]=\"suffixIcon\" />\n          } @else {\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"lucide lucide-chevron-down text-muted-foreground size-4 opacity-50\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"m6 9 6 6 6-6\" />\n            </svg>\n          }\n        </span>\n      </button>\n\n      <ui-popover-content class=\"p-0\" align=\"start\" [sideOffset]=\"4\" (closeAutoFocus)=\"mounted = false\">\n        <!-- Radix mounts content only while open (kept through the exit animation): 10k options must not render up front. -->\n        @if (renderContent) {\n          <ui-command\n            uiAdvanceSelectPanel\n            [listHeight]=\"listHeight\"\n            (popupScroll)=\"popupScroll.emit($event)\"\n            [shouldFilter]=\"false\"\n            class=\"flex flex-col overflow-hidden\"\n          >\n            @if (showSearchInput) {\n              <ui-command-input\n                [value]=\"query\"\n                (valueChange)=\"setQuery($event)\"\n                [placeholder]=\"placeholder\"\n                (keydown)=\"handleInputKeydown($event)\"\n              />\n            }\n\n            <ui-command-list class=\"flex-1 overflow-y-auto\">\n              @if (!loading && filteredOptions.length === 0) {\n                <ui-command-empty>\n                  @if (isTemplate(emptyContent ?? notFoundContent)) {\n                    <ng-container [uiAdvanceSelectOutlet]=\"asTemplate(emptyContent ?? notFoundContent)\" />\n                  } @else {\n                    {{ emptyContent ?? notFoundContent }}\n                  }\n                </ui-command-empty>\n              }\n\n              @if (loading && filteredOptions.length === 0) {\n                <div class=\"py-6 text-center text-sm\">\n                  @if (isTemplate(loadingText)) {\n                    <ng-container [uiAdvanceSelectOutlet]=\"asTemplate(loadingText)\" />\n                  } @else {\n                    {{ loadingText }}\n                  }\n                </div>\n              }\n\n              @for (group of grouped; track group.heading; let gi = $index) {\n                @if (gi > 0) {\n                  <ui-command-separator />\n                }\n                <ui-command-group [heading]=\"group.heading || undefined\">\n                  @for (opt of group.items; track tagKey(opt); let idx = $index) {\n                    <ui-command-item\n                      [value]=\"tagKey(opt)\"\n                      [disabled]=\"isDisabledOption(opt) || (atMax && !isSelected(opt))\"\n                      [attr.data-active]=\"gi === 0 && idx === 0 && defaultActiveFirstOption ? 'true' : null\"\n                      [class]=\"virtual && options.length > 100 ? '[content-visibility:auto]' : ''\"\n                      (select)=\"selectOption(opt)\"\n                    >\n                      <svg\n                        xmlns=\"http://www.w3.org/2000/svg\"\n                        width=\"24\"\n                        height=\"24\"\n                        viewBox=\"0 0 24 24\"\n                        fill=\"none\"\n                        stroke=\"currentColor\"\n                        stroke-width=\"2\"\n                        stroke-linecap=\"round\"\n                        stroke-linejoin=\"round\"\n                        [attr.class]=\"checkClass(opt)\"\n                        aria-hidden=\"true\"\n                      >\n                        <path d=\"M20 6 9 17l-5-5\" />\n                      </svg>\n                      @if (renderOption) {\n                        <ng-container\n                          [uiAdvanceSelectOutlet]=\"renderOption\"\n                          [uiAdvanceSelectOutletContext]=\"{ option: opt, index: idx }\"\n                        />\n                      } @else {\n                        {{ getLabel(opt) }}\n                      }\n                    </ui-command-item>\n                  }\n                </ui-command-group>\n              }\n\n              @if (showCreateItem) {\n                <ui-command-item [value]=\"'create:' + query\" (select)=\"createTag()\">\n                  <svg\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    width=\"24\"\n                    height=\"24\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    stroke-width=\"2\"\n                    stroke-linecap=\"round\"\n                    stroke-linejoin=\"round\"\n                    class=\"lucide lucide-check mr-2 size-4 opacity-0\"\n                    aria-hidden=\"true\"\n                  >\n                    <path d=\"M20 6 9 17l-5-5\" />\n                  </svg>\n                  Create &quot;{{ query.trim() }}&quot;\n                </ui-command-item>\n              }\n            </ui-command-list>\n\n            @if (isMultiple && selectedOptions.length > 0) {\n              <div class=\"flex items-center justify-between border-t px-2 py-1.5 text-xs\">\n                <span class=\"text-muted-foreground\">{{ selectedOptions.length }} selected</span>\n                <button type=\"button\" class=\"text-muted-foreground hover:text-foreground\" (click)=\"clearAll($event)\">\n                  Clear all\n                </button>\n              </div>\n            }\n          </ui-command>\n        }\n      </ui-popover-content>\n    </ui-popover>\n  `,\n})\nexport class UiAdvanceSelectComponent<T extends AdvanceSelectOption = AdvanceSelectOption>\n  implements OnChanges, AfterViewInit\n{\n  /** Controlled selection: a value (single) or an array (multiple / tags). */\n  @Input() value?: unknown\n  @Input() options: T[] = []\n\n  // Mode\n  @Input() mode: AdvanceSelectMode = 'single'\n\n  // Field mapping\n  @Input() fieldNames: AdvanceSelectFieldNames = {}\n\n  // Appearance\n  @Input() size: AdvanceSelectSize = 'default'\n  @Input() variant: AdvanceSelectVariant = 'outlined'\n  @Input() status: AdvanceSelectStatus = 'default'\n  @Input() placeholder = 'Select...'\n\n  // Search\n  @Input({ transform: booleanAttribute }) showSearch = false\n  /** Controlled search query (pair with `searchChange`). */\n  @Input() searchValue?: string\n  @Input({ transform: booleanAttribute }) autoClearSearchValue = true\n  @Input() filterOption: boolean | ((input: string, option: T) => boolean) = true\n  @Input() optionFilterProp: string | string[] = 'label'\n  @Input() filterSort?: (optionA: T, optionB: T, info: { searchValue: string }) => number\n\n  // Multiple / tags\n  @Input() maxCount?: number\n  @Input() maxTagCount?: number\n  @Input() maxTagTextLength?: number\n  @Input() maxTagPlaceholder?: string | ((omittedValues: T[]) => string)\n  @Input() tokenSeparators: string[] = []\n  @Input({ transform: booleanAttribute }) hideSelected = false\n  @Input({ transform: booleanAttribute }) allowCreate = false\n\n  // State\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input({ transform: booleanAttribute }) allowClear = true\n  /** Controlled open state (pair with `openChange`). Leave unset for uncontrolled use. */\n  @Input() open?: boolean\n  @Input({ transform: booleanAttribute }) defaultOpen = false\n  @Input({ transform: booleanAttribute }) defaultActiveFirstOption = true\n\n  // Customization\n  @Input() notFoundContent: AdvanceSelectContent = 'No results.'\n  @Input() loadingText: AdvanceSelectContent = 'Loading...'\n  @Input() listHeight = 300\n  @Input({ transform: booleanAttribute }) virtual = true\n\n  @Input('class') className?: string\n\n  // Render slots (React ReactNode / render props -> TemplateRef)\n  @Input() prefix?: TemplateRef<unknown> | null\n  @Input() suffix?: TemplateRef<unknown> | null\n  @Input() suffixIcon?: TemplateRef<unknown> | null\n  @Input() clearIcon?: TemplateRef<unknown> | null\n  @Input() renderLabel?: TemplateRef<unknown> | null\n  @Input() renderTag?: TemplateRef<unknown> | null\n  @Input() renderOption?: TemplateRef<unknown> | null\n  @Input() emptyContent?: AdvanceSelectContent\n\n  // Events\n  /** React `onValueChange(value, option)`: the value (two-way `[(value)]`). */\n  @Output() valueChange = new EventEmitter<unknown>()\n  /** React `onValueChange`'s second argument: `{ value, option }` for every value change. */\n  @Output() optionChange = new EventEmitter<{ value: unknown; option: T | T[] | undefined }>()\n  @Output() searchChange = new EventEmitter<string>()\n  @Output() openChange = new EventEmitter<boolean>()\n  @Output() select = new EventEmitter<{ value: unknown; option: T }>()\n  @Output() deselect = new EventEmitter<{ value: unknown; option: T }>()\n  @Output() clear = new EventEmitter<void>()\n  @Output() focus = new EventEmitter<FocusEvent>()\n  @Output() blur = new EventEmitter<FocusEvent>()\n  @Output() popupScroll = new EventEmitter<Event>()\n  @Output() inputKeyDown = new EventEmitter<KeyboardEvent>()\n\n  private readonly internalOpen = signal<boolean | null>(null)\n  private readonly internalQuery = signal('')\n\n  readonly clearAllFn = (e: Event) => this.clearAll(e)\n  /** True from open until the exit animation finishes (Radix Presence). */\n  mounted = false\n\n  get renderContent(): boolean {\n    if (this.isOpen) this.mounted = true\n    return this.mounted\n  }\n\n  @ViewChild('triggerEl', { static: true }) private triggerEl?: ElementRef<HTMLButtonElement>\n\n  ngAfterViewInit(): void {\n    // Radix Slot lets the child's props win over PopoverTrigger's (data-slot=\"popover-trigger\");\n    // Angular host bindings win over template attributes, so restore the child's slot here.\n    this.triggerEl?.nativeElement.setAttribute('data-slot', 'advance-select')\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    // React clears the query whenever the popover closes (controlled or not).\n    const open = changes['open']\n    if (open && !open.firstChange && !open.currentValue && this.autoClearSearchValue) this.setQuery('')\n  }\n\n  get isOpen(): boolean {\n    if (this.open !== undefined) return this.open\n    return this.internalOpen() ?? this.defaultOpen\n  }\n\n  setOpen(v: boolean): void {\n    if (this.open === undefined) this.internalOpen.set(v)\n    this.openChange.emit(v)\n    if (!v && this.autoClearSearchValue) this.setQuery('')\n  }\n\n  get query(): string {\n    return this.searchValue !== undefined ? this.searchValue : this.internalQuery()\n  }\n\n  setQuery(v: string): void {\n    if (this.searchValue === undefined) this.internalQuery.set(v)\n    this.searchChange.emit(v)\n  }\n\n  private get valueKey(): string {\n    return this.fieldNames.value ?? 'value'\n  }\n  private get labelKey(): string {\n    return this.fieldNames.label ?? 'label'\n  }\n  private get groupKey(): string {\n    return this.fieldNames.group ?? 'group'\n  }\n  private get disabledKey(): string {\n    return this.fieldNames.disabled ?? 'disabled'\n  }\n\n  getValue(o: T): unknown {\n    return readKey(o, this.valueKey, o)\n  }\n  getLabel(o: T): string {\n    return String(readKey(o, this.labelKey, ''))\n  }\n  private getGroup(o: T): string | undefined {\n    const g = readKey(o, this.groupKey)\n    return g == null ? undefined : String(g)\n  }\n  isDisabledOption(o: T): boolean {\n    return Boolean(readKey(o, this.disabledKey, false))\n  }\n  tagKey(o: T): string {\n    return String(this.getValue(o))\n  }\n\n  get isMultiple(): boolean {\n    return this.mode === 'multiple' || this.mode === 'tags'\n  }\n\n  get selectedValues(): unknown[] {\n    if (this.value == null) return []\n    if (this.isMultiple) return Array.isArray(this.value) ? this.value : []\n    return [this.value]\n  }\n\n  isSelected(o: T): boolean {\n    return this.selectedValues.includes(this.getValue(o))\n  }\n\n  get selectedOptions(): T[] {\n    return this.selectedValues.map((v) => {\n      const found = this.options.find((o) => this.getValue(o) === v)\n      if (found) return found\n      // For created tags not in options, create a minimal option object\n      return { [this.labelKey]: String(v), [this.valueKey]: v } as T\n    })\n  }\n\n  private getOptionByValue(v: unknown): T | undefined {\n    return this.options.find((o) => this.getValue(o) === v)\n  }\n\n  private matchesFilter(o: T, q: string): boolean {\n    if (typeof this.filterOption === 'function') return this.filterOption(q, o)\n    if (this.filterOption === false) return true\n    const label = this.getLabel(o).toLowerCase()\n    const search = q.toLowerCase()\n    const props = Array.isArray(this.optionFilterProp) ? this.optionFilterProp : [this.optionFilterProp]\n    for (const prop of props) {\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  get filteredOptions(): T[] {\n    let result = this.options\n    const q = this.query.trim()\n    if (q) result = result.filter((o) => this.matchesFilter(o, q))\n    if (this.hideSelected && this.isMultiple) {\n      const selected = new Set(this.selectedValues)\n      result = result.filter((o) => !selected.has(this.getValue(o)))\n    }\n    if (q && this.filterSort) {\n      const sort = this.filterSort\n      result = [...result].sort((a, b) => sort(a, b, { searchValue: q }))\n    }\n    return result\n  }\n\n  get grouped(): { heading: string; items: T[] }[] {\n    const groups = new Map<string, T[]>()\n    for (const opt of this.filteredOptions) {\n      const key = this.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  }\n\n  get atMax(): boolean {\n    if (typeof this.maxCount !== 'number') return false\n    const count = Array.isArray(this.value) ? this.value.length : this.value ? 1 : 0\n    return count >= this.maxCount\n  }\n\n  get visibleTags(): T[] {\n    if (!this.isMultiple) return []\n    const opts = this.selectedOptions\n    return typeof this.maxTagCount === 'number' ? opts.slice(0, this.maxTagCount) : opts\n  }\n\n  get hiddenTagCount(): number {\n    if (!this.isMultiple || typeof this.maxTagCount !== 'number') return 0\n    return Math.max(0, this.selectedOptions.length - this.maxTagCount)\n  }\n\n  get maxTagPlaceholderText(): string {\n    const p = this.maxTagPlaceholder\n    if (typeof p === 'function') return p(this.selectedOptions.slice(this.maxTagCount ?? 0))\n    return p ? p : `+${this.hiddenTagCount}`\n  }\n\n  displayLabel(o: T): string {\n    let label = this.getLabel(o)\n    if (this.maxTagTextLength && label.length > this.maxTagTextLength) {\n      label = label.slice(0, this.maxTagTextLength) + '...'\n    }\n    return label\n  }\n\n  tagContext(o: T): AdvanceSelectTagContext {\n    return {\n      value: this.getValue(o),\n      label: this.displayLabel(o),\n      closable: !this.disabled,\n      onClose: (e?: Event) => this.removeTag(this.getValue(o), e),\n    }\n  }\n\n  get labelContext(): AdvanceSelectLabelContext {\n    const first = this.selectedOptions[0]\n    return { value: this.value, label: first ? this.getLabel(first) : '' }\n  }\n\n  removeTagFn(o: T): (e: Event) => void {\n    return (e) => this.removeTag(this.getValue(o), e)\n  }\n\n  get isEmpty(): boolean {\n    return this.isMultiple\n      ? !Array.isArray(this.value) || this.value.length === 0\n      : this.value == null || this.value === ''\n  }\n\n  get showClear(): boolean {\n    return this.allowClear && !this.isEmpty && !this.disabled && !this.loading\n  }\n\n  get showSearchInput(): boolean {\n    return this.showSearch || this.mode === 'tags'\n  }\n\n  get showCreateItem(): boolean {\n    const q = this.query.trim()\n    return !!q && (this.allowCreate || this.mode === 'tags') && !this.options.some((o) => this.getLabel(o) === q)\n  }\n\n  get triggerClass(): string {\n    return 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[this.size],\n      variantClasses[this.variant],\n      statusClasses[this.status],\n      this.className,\n    )\n  }\n\n  get singleLabelClass(): string {\n    return cn('flex-1 truncate text-left', this.selectedOptions.length ? 'text-foreground' : 'text-muted-foreground')\n  }\n\n  checkClass(o: T): string {\n    return cn('lucide lucide-check mr-2 size-4 shrink-0', this.isSelected(o) ? 'opacity-100' : 'opacity-0')\n  }\n\n  isTemplate(v: unknown): boolean {\n    return v instanceof TemplateRef\n  }\n\n  asTemplate(v: unknown): TemplateRef<unknown> {\n    return v as TemplateRef<unknown>\n  }\n\n  private emitValue(v: unknown, option: T | T[] | undefined): void {\n    this.valueChange.emit(v)\n    this.optionChange.emit({ value: v, option })\n  }\n\n  selectOption(option: T): void {\n    if (this.isDisabledOption(option)) return\n    const v = this.getValue(option)\n\n    if (!this.isMultiple) {\n      this.emitValue(v, option)\n      this.select.emit({ value: v, option })\n      this.setOpen(false)\n      if (this.autoClearSearchValue) this.setQuery('')\n      return\n    }\n\n    const current = Array.isArray(this.value) ? [...this.value] : []\n    if (this.selectedValues.includes(v)) {\n      this.emitValue(\n        current.filter((x) => x !== v),\n        option,\n      )\n      this.deselect.emit({ value: v, option })\n    } else {\n      if (this.atMax) return\n      this.emitValue([...current, v], option)\n      this.select.emit({ value: v, option })\n    }\n    if (this.autoClearSearchValue) this.setQuery('')\n  }\n\n  removeTag(value: unknown, event?: Event): void {\n    event?.stopPropagation()\n    if (this.disabled) return\n    const current = Array.isArray(this.value) ? [...this.value] : []\n    const option = this.getOptionByValue(value)\n    this.emitValue(\n      current.filter((x) => x !== value),\n      option,\n    )\n    if (option) this.deselect.emit({ value, option })\n  }\n\n  clearAll(event?: Event): void {\n    event?.stopPropagation()\n    if (this.disabled) return\n    this.clear.emit()\n    if (this.isMultiple) this.emitValue([], [])\n    else this.emitValue(null, undefined)\n    this.setQuery('')\n  }\n\n  createTag(): void {\n    if (!this.allowCreate && this.mode !== 'tags') return\n    const q = this.query.trim()\n    if (!q) return\n    if (this.options.some((o) => this.getLabel(o) === q || String(this.getValue(o)) === q)) return\n    const newOption = { [this.labelKey]: q, [this.valueKey]: q } as T\n\n    if (!this.isMultiple) {\n      this.emitValue(q, newOption)\n      this.select.emit({ value: q, option: newOption })\n      this.setOpen(false)\n      this.setQuery('')\n      return\n    }\n\n    if (this.atMax) return\n    const current = Array.isArray(this.value) ? [...this.value] : []\n    this.emitValue([...current, q], newOption)\n    this.select.emit({ value: q, option: newOption })\n    this.setQuery('')\n  }\n\n  handleInputKeydown(event: KeyboardEvent): void {\n    this.inputKeyDown.emit(event)\n    if (event.key === 'Enter' && this.query.trim() && this.mode === 'tags') {\n      event.preventDefault()\n      this.createTag()\n    }\n    if (this.tokenSeparators.length && this.mode === 'tags' && this.tokenSeparators.includes(event.key)) {\n      event.preventDefault()\n      this.createTag()\n    }\n  }\n\n  /** Enter / Space on the role=button spans inside the trigger (React onKeyDown). */\n  onActionKeydown(event: KeyboardEvent, action: (e: Event) => void): void {\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      action(event)\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/advance-select/advance-select.component.ts"
    },
    {
      "path": "packages/registry-angular/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": "~/src/app/components/ui/advance-select/types.ts"
    },
    {
      "path": "packages/registry-angular/components/advance-select/index.ts",
      "content": "export {\n  UiAdvanceSelectComponent,\n  UiAdvanceSelectOutletDirective,\n  UiAdvanceSelectPanelDirective,\n} from './advance-select.component'\nexport type {\n  AdvanceSelectContent,\n  AdvanceSelectLabelContext,\n  AdvanceSelectMode,\n  AdvanceSelectOption,\n  AdvanceSelectOptionContext,\n  AdvanceSelectSize,\n  AdvanceSelectStatus,\n  AdvanceSelectTagContext,\n  AdvanceSelectVariant,\n} from './advance-select.component'\nexport { readKey, type AdvanceSelectFieldNames, type SelectOption } from './types'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/advance-select/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popover.json",
    "https://uipkge.dev/r/angular/command.json",
    "https://uipkge.dev/r/angular/badge.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"
  ]
}