{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-model-selector",
  "type": "registry:component",
  "title": "Model Selector",
  "description": "The props-driven model picker: search, groups, and reasoning effort levels, no runtime required.",
  "dependencies": [
    "lucide-react",
    "class-variance-authority",
    "radix-ui"
  ],
  "registryDependencies": [
    "command",
    "popover"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/model-selector.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/model-selector.radix.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  createContext,\n  useContext,\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n} from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { CheckIcon, ChevronDownIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} 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 { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\n\nexport type ModelSelectorEffortOption = {\n  id: string;\n  name: string;\n};\n\nexport const DEFAULT_EFFORT_OPTIONS: readonly ModelSelectorEffortOption[] = [\n  { id: \"low\", name: \"Low\" },\n  { id: \"medium\", name: \"Med\" },\n  { id: \"high\", name: \"High\" },\n];\n\nexport type ModelOption = {\n  id: string;\n  name: string;\n  description?: string;\n  icon?: ReactNode;\n  disabled?: boolean;\n  /** Extra terms matched by ModelSelector.Search, in addition to id and name. */\n  keywords?: readonly string[];\n  /**\n   * Reasoning effort levels the model supports. Pass `true` for the default\n   * low/medium/high levels, or a custom list. Omit for models without\n   * configurable reasoning.\n   */\n  efforts?: boolean | readonly ModelSelectorEffortOption[];\n};\n\nfunction getModelEfforts(\n  model: ModelOption | undefined,\n): readonly ModelSelectorEffortOption[] | undefined {\n  if (!model?.efforts) return undefined;\n  return model.efforts === true ? DEFAULT_EFFORT_OPTIONS : model.efforts;\n}\n\nfunction resolveEffort(\n  efforts: readonly ModelSelectorEffortOption[] | undefined,\n  effort: string | undefined,\n): string | undefined {\n  if (effort === undefined) return undefined;\n  return efforts?.some((e) => e.id === effort) ? effort : undefined;\n}\n\n/**\n * Returns the effort id if the given model supports it, otherwise undefined.\n * Effort selection is kept sticky across model switches; this resolves what\n * actually applies to the current model.\n */\nexport function resolveModelEffort(\n  models: readonly ModelOption[],\n  modelId: string | undefined,\n  effort: string | undefined,\n): string | undefined {\n  return resolveEffort(\n    getModelEfforts(models.find((m) => m.id === modelId)),\n    effort,\n  );\n}\n\nfunction useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange,\n}: {\n  prop: T | undefined;\n  defaultProp: T | undefined;\n  onChange: ((next: T) => void) | undefined;\n}) {\n  const [internal, setInternal] = useState(defaultProp);\n  const isControlled = prop !== undefined;\n  const value = isControlled ? prop : internal;\n  // Read onChange through a ref so inline callbacks don't recreate the setter\n  // (and with it the memoized context value) every render.\n  const onChangeRef = useRef(onChange);\n  useEffect(() => {\n    onChangeRef.current = onChange;\n  });\n  const setValue = useCallback(\n    (next: T) => {\n      if (!isControlled) setInternal(next);\n      onChangeRef.current?.(next);\n    },\n    [isControlled],\n  );\n  return [value, setValue] as const;\n}\n\ntype ModelSelectorContextValue = {\n  models: readonly ModelOption[];\n  value: string | undefined;\n  setValue: (value: string) => void;\n  /** The model matching `value`, derived once for all sub-components. */\n  selectedModel: ModelOption | undefined;\n  /** The selected model's effort levels, undefined when not configurable. */\n  efforts: readonly ModelSelectorEffortOption[] | undefined;\n  /** Effort resolved against the selected model's supported levels. */\n  effort: string | undefined;\n  setEffort: (effort: string) => void;\n  setOpen: (open: boolean) => void;\n};\n\nconst ModelSelectorContext = createContext<ModelSelectorContextValue | null>(\n  null,\n);\n\nexport function useModelSelectorContext() {\n  const ctx = useContext(ModelSelectorContext);\n  if (!ctx) {\n    throw new Error(\n      \"ModelSelector sub-components must be used within ModelSelector.Root\",\n    );\n  }\n  return ctx;\n}\n\n/**\n * The selected model's effort levels and the active selection. Use it to build\n * a custom effort UI inside ModelSelector.Content (e.g. a slider or a shadcn\n * DropdownMenu) when the built-in ModelSelector.Effort layout doesn't fit.\n * `efforts` is undefined for models without configurable reasoning.\n */\nexport function useModelSelectorEfforts(): {\n  efforts: readonly ModelSelectorEffortOption[] | undefined;\n  effort: string | undefined;\n  setEffort: (effort: string) => void;\n} {\n  const { efforts, effort, setEffort } = useModelSelectorContext();\n  return { efforts, effort, setEffort };\n}\n\nexport type ModelSelectorRootProps = {\n  models: readonly ModelOption[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  effort?: string;\n  defaultEffort?: string;\n  onEffortChange?: (effort: string) => void;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  children: ReactNode;\n};\n\nfunction ModelSelectorRoot({\n  models,\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  effort: effortProp,\n  defaultEffort,\n  onEffortChange,\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  children,\n}: ModelSelectorRootProps) {\n  const [value, setValue] = useControllableState({\n    prop: valueProp,\n    defaultProp: defaultValue ?? models[0]?.id,\n    onChange: onValueChange,\n  });\n  const [effort, setEffort] = useControllableState({\n    prop: effortProp,\n    defaultProp: defaultEffort,\n    onChange: onEffortChange,\n  });\n  const [open, setOpen] = useControllableState({\n    prop: openProp,\n    defaultProp: defaultOpen ?? false,\n    onChange: onOpenChange,\n  });\n\n  const selectedModel = models.find((m) => m.id === value);\n  const efforts = getModelEfforts(selectedModel);\n  const activeEffort = resolveEffort(efforts, effort);\n  const contextValue = useMemo(\n    () => ({\n      models,\n      value,\n      setValue,\n      selectedModel,\n      efforts,\n      effort: activeEffort,\n      setEffort,\n      setOpen,\n    }),\n    [\n      models,\n      value,\n      setValue,\n      selectedModel,\n      efforts,\n      activeEffort,\n      setEffort,\n      setOpen,\n    ],\n  );\n\n  return (\n    <ModelSelectorContext.Provider value={contextValue}>\n      <Popover open={open ?? false} onOpenChange={setOpen}>\n        {children}\n      </Popover>\n    </ModelSelectorContext.Provider>\n  );\n}\n\nexport const modelSelectorTriggerVariants = cva(\n  \"focus-visible:ring-ring/50 flex w-fit items-center justify-between gap-2 overflow-hidden rounded-md text-sm whitespace-nowrap transition-colors outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5\",\n  {\n    variants: {\n      variant: {\n        outline:\n          \"border-input hover:bg-accent hover:text-accent-foreground border bg-transparent\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        muted: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n      },\n      size: {\n        default: \"h-9 px-3 py-2\",\n        sm: \"h-8 px-2.5 py-1.5 text-xs\",\n        lg: \"h-10 px-4 py-2.5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"outline\",\n      size: \"default\",\n    },\n  },\n);\n\nexport type ModelSelectorTriggerProps = ComponentPropsWithoutRef<\n  typeof PopoverTrigger\n> &\n  VariantProps<typeof modelSelectorTriggerVariants>;\n\nfunction ModelSelectorTrigger({\n  className,\n  variant,\n  size,\n  children,\n  onKeyDown,\n  ...props\n}: ModelSelectorTriggerProps) {\n  const { setOpen } = useModelSelectorContext();\n\n  return (\n    <PopoverTrigger\n      data-slot=\"model-selector-trigger\"\n      data-variant={variant ?? \"outline\"}\n      data-size={size ?? \"default\"}\n      role=\"combobox\"\n      aria-haspopup=\"listbox\"\n      className={cn(modelSelectorTriggerVariants({ variant, size }), className)}\n      onKeyDown={(e) => {\n        onKeyDown?.(e);\n        if (e.defaultPrevented) return;\n        // ARIA combobox: arrows open the listbox from a focused trigger.\n        // Popover leaves this to the consumer.\n        if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n          e.preventDefault();\n          setOpen(true);\n        }\n      }}\n      {...props}\n    >\n      {children ?? <ModelSelectorValue />}\n      <ChevronDownIcon className=\"size-4 opacity-50\" />\n    </PopoverTrigger>\n  );\n}\n\nexport type ModelSelectorValueProps = {\n  placeholder?: ReactNode;\n  /** Show the active effort level next to the model name. */\n  showEffort?: boolean;\n  className?: string;\n};\n\nfunction ModelIcon({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <span\n      className={cn(\n        \"flex size-3.5 shrink-0 items-center justify-center [&_svg]:size-3.5\",\n        className,\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n\nfunction ModelSelectorValue({\n  placeholder = \"Select model\",\n  showEffort = true,\n  className,\n}: ModelSelectorValueProps) {\n  const { selectedModel, efforts, effort } = useModelSelectorContext();\n\n  if (!selectedModel) {\n    return (\n      <span\n        data-slot=\"model-selector-value\"\n        className={cn(\"text-muted-foreground\", className)}\n      >\n        {placeholder}\n      </span>\n    );\n  }\n\n  const effortName =\n    showEffort && effort !== undefined\n      ? efforts?.find((e) => e.id === effort)?.name\n      : undefined;\n\n  return (\n    <span\n      data-slot=\"model-selector-value\"\n      className={cn(\"flex min-w-0 items-center gap-2\", className)}\n    >\n      {selectedModel.icon && <ModelIcon>{selectedModel.icon}</ModelIcon>}\n      <span className=\"truncate font-medium\">{selectedModel.name}</span>\n      {effortName && (\n        <span className=\"text-muted-foreground min-w-7.5 truncate text-center\">\n          {effortName}\n        </span>\n      )}\n    </span>\n  );\n}\n\nexport type ModelSelectorContentProps = Omit<\n  ComponentPropsWithoutRef<typeof PopoverContent>,\n  \"side\"\n> & {\n  /**\n   * Preferred side for the initial placement. Once the popover is open, the\n   * rendered side takes over until it closes, so the popup does not jump\n   * between sides while filtering resizes the list.\n   */\n  side?: ComponentPropsWithoutRef<typeof PopoverContent>[\"side\"];\n  searchable?: boolean;\n};\n\n// The popover re-evaluates collision flipping whenever the popup resizes, so\n// filtering the list down flips the popup back to the preferred side\n// mid-interaction. Feed the rendered side back as the preferred side, making\n// the popup keep its side until it no longer fits.\nfunction useLazyFlipSide(): {\n  side: ModelSelectorContentProps[\"side\"];\n  popupRef: (node: HTMLDivElement | null) => void;\n} {\n  const [side, setSide] = useState<ModelSelectorContentProps[\"side\"]>();\n  const observerRef = useRef<MutationObserver | null>(null);\n  const popupRef = useCallback((node: HTMLDivElement | null) => {\n    observerRef.current?.disconnect();\n    observerRef.current = null;\n    if (!node) {\n      setSide(undefined);\n      return;\n    }\n    const sync = () => {\n      const rendered = node.getAttribute(\"data-side\");\n      if (rendered) setSide(rendered as ModelSelectorContentProps[\"side\"]);\n    };\n    sync();\n    const observer = new MutationObserver(sync);\n    observer.observe(node, {\n      attributes: true,\n      attributeFilter: [\"data-side\"],\n    });\n    observerRef.current = observer;\n  }, []);\n  return { side, popupRef };\n}\n\n/**\n * Hidden input that anchors cmdk's keyboard navigation, keeping the list\n * keyboard-operable without a visible search box. ModelSelectorContent renders\n * one automatically when unfiltered.\n */\nfunction ModelSelectorFocusAnchor() {\n  return (\n    <div className=\"sr-only\">\n      <CommandInput readOnly aria-label=\"Model\" />\n    </div>\n  );\n}\n\nfunction ModelSelectorContent({\n  className,\n  align = \"start\",\n  side,\n  sideOffset = 6,\n  searchable,\n  children,\n  ...props\n}: ModelSelectorContentProps) {\n  const { value } = useModelSelectorContext();\n  const { side: renderedSide, popupRef } = useLazyFlipSide();\n  const unfiltered =\n    searchable === false || (!searchable && children === undefined);\n\n  return (\n    <PopoverContent\n      ref={popupRef}\n      data-slot=\"model-selector-content\"\n      align={align}\n      side={renderedSide ?? side ?? \"bottom\"}\n      sideOffset={sideOffset}\n      className={cn(\n        \"bg-popover w-72 min-w-(--radix-popover-trigger-width) overflow-hidden rounded-xl p-0\",\n        className,\n      )}\n      {...props}\n    >\n      <Command\n        className=\"bg-transparent\"\n        shouldFilter={!unfiltered}\n        {...(value !== undefined ? { defaultValue: value } : {})}\n      >\n        {unfiltered && <ModelSelectorFocusAnchor />}\n        {children ?? (\n          <>\n            {searchable && <ModelSelectorSearch />}\n            <ModelSelectorList />\n            <ModelSelectorEffort />\n          </>\n        )}\n      </Command>\n    </PopoverContent>\n  );\n}\n\nexport type ModelSelectorSearchProps = ComponentPropsWithoutRef<\n  typeof CommandInput\n>;\n\nfunction ModelSelectorSearch({\n  placeholder = \"Search models...\",\n  ...props\n}: ModelSelectorSearchProps) {\n  return (\n    <CommandInput\n      data-slot=\"model-selector-search\"\n      placeholder={placeholder}\n      {...props}\n    />\n  );\n}\n\nexport type ModelSelectorListProps = ComponentPropsWithoutRef<\n  typeof CommandList\n>;\n\nfunction ModelSelectorList({\n  className,\n  children,\n  ...props\n}: ModelSelectorListProps) {\n  const { models } = useModelSelectorContext();\n\n  return (\n    <CommandList\n      data-slot=\"model-selector-list\"\n      className={cn(\n        \"[-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <ModelSelectorEmpty />\n          <CommandGroup>\n            {models.map((model) => (\n              <ModelSelectorItem key={model.id} model={model} />\n            ))}\n          </CommandGroup>\n        </>\n      )}\n    </CommandList>\n  );\n}\n\nexport type ModelSelectorEmptyProps = ComponentPropsWithoutRef<\n  typeof CommandEmpty\n>;\n\nfunction ModelSelectorEmpty({ children, ...props }: ModelSelectorEmptyProps) {\n  return (\n    <CommandEmpty data-slot=\"model-selector-empty\" {...props}>\n      {children ?? \"No models found.\"}\n    </CommandEmpty>\n  );\n}\n\nexport type ModelSelectorGroupProps = ComponentPropsWithoutRef<\n  typeof CommandGroup\n>;\n\nfunction ModelSelectorGroup(props: ModelSelectorGroupProps) {\n  return <CommandGroup data-slot=\"model-selector-group\" {...props} />;\n}\n\nexport type ModelSelectorSeparatorProps = ComponentPropsWithoutRef<\n  typeof CommandSeparator\n>;\n\nfunction ModelSelectorSeparator(props: ModelSelectorSeparatorProps) {\n  return <CommandSeparator data-slot=\"model-selector-separator\" {...props} />;\n}\n\nexport type ModelSelectorItemProps = Omit<\n  ComponentPropsWithoutRef<typeof CommandItem>,\n  \"value\"\n> & {\n  model: ModelOption;\n};\n\nfunction ModelSelectorItem({\n  model,\n  className,\n  children,\n  onSelect,\n  ...props\n}: ModelSelectorItemProps) {\n  const { value, setValue, setOpen } = useModelSelectorContext();\n  const isSelected = value === model.id;\n\n  return (\n    <CommandItem\n      data-slot=\"model-selector-item\"\n      value={model.id}\n      keywords={[model.name, ...(model.keywords ?? [])]}\n      {...(model.disabled ? { disabled: true } : undefined)}\n      onSelect={(selectedValue) => {\n        setValue(model.id);\n        setOpen(false);\n        onSelect?.(selectedValue);\n      }}\n      className={cn(\n        \"relative items-start gap-2 rounded-lg py-2 ps-3 pe-9 [&_svg:not([class*='size-'])]:size-3.5\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          {model.icon && (\n            <ModelIcon className=\"mt-[3px]\">{model.icon}</ModelIcon>\n          )}\n          <span className=\"flex min-w-0 flex-col\">\n            <span className=\"truncate font-medium\">{model.name}</span>\n            {model.description && (\n              <span className=\"text-muted-foreground truncate text-xs\">\n                {model.description}\n              </span>\n            )}\n          </span>\n        </>\n      )}\n      {isSelected && (\n        <span className=\"absolute end-3 top-2.5 flex size-4 items-center justify-center\">\n          <CheckIcon className=\"size-4\" />\n        </span>\n      )}\n    </CommandItem>\n  );\n}\n\nexport type ModelSelectorEffortProps = ComponentPropsWithoutRef<\"div\"> & {\n  label?: ReactNode;\n};\n\nfunction ModelSelectorEffort({\n  label = \"Thinking\",\n  className,\n  onKeyDown,\n  ...props\n}: ModelSelectorEffortProps) {\n  const { efforts, effort, setEffort } = useModelSelectorEfforts();\n\n  if (!efforts?.length) return null;\n\n  return (\n    <div\n      data-slot=\"model-selector-effort\"\n      className={cn(\n        \"flex cursor-default items-center justify-between gap-3 border-t px-3 py-2\",\n        className,\n      )}\n      onKeyDown={(e) => {\n        onKeyDown?.(e);\n        if (e.defaultPrevented) return;\n        // cmdk's Command root claims Home/End to jump the model list; stop\n        // them here so only the radiogroup reacts.\n        if (e.key === \"Home\" || e.key === \"End\") e.stopPropagation();\n        // Vertical arrows refocus cmdk's input before the event bubbles to\n        // the Command root: the same keypress then moves the list highlight,\n        // and Enter selects again (cmdk's Enter is inert while a radio has\n        // focus, so the highlight would otherwise move with no way to act).\n        if (e.key === \"ArrowUp\" || e.key === \"ArrowDown\") {\n          e.currentTarget\n            .closest(\"[cmdk-root]\")\n            ?.querySelector<HTMLInputElement>(\"[cmdk-input]\")\n            ?.focus();\n        }\n      }}\n      {...props}\n    >\n      <span className=\"text-muted-foreground text-xs\">{label}</span>\n      <RadioGroupPrimitive.Root\n        value={effort ?? \"\"}\n        onValueChange={setEffort}\n        orientation=\"horizontal\"\n        aria-label={typeof label === \"string\" ? label : \"Reasoning effort\"}\n        className=\"flex items-center gap-0.5\"\n      >\n        {efforts.map((option) => (\n          <RadioGroupPrimitive.Item\n            key={option.id}\n            value={option.id}\n            className={cn(\n              \"focus-visible:ring-ring/50 text-muted-foreground hover:bg-muted hover:text-foreground cursor-pointer rounded-md px-2 py-1 text-xs transition-colors outline-none focus-visible:ring-1\",\n              \"data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground data-[state=checked]:font-medium\",\n            )}\n          >\n            {option.name}\n          </RadioGroupPrimitive.Item>\n        ))}\n      </RadioGroupPrimitive.Root>\n    </div>\n  );\n}\n\nexport type ModelSelectorProps = Omit<ModelSelectorRootProps, \"children\"> &\n  VariantProps<typeof modelSelectorTriggerVariants> & {\n    /** Render a search input above the model list. */\n    searchable?: boolean;\n    /** Alignment of the dropdown relative to the trigger. Use `\"end\"` when the\n     * trigger sits at the right edge of its container. */\n    align?: ModelSelectorContentProps[\"align\"];\n    className?: string;\n    contentClassName?: string;\n  };\n\nexport {\n  ModelSelectorRoot,\n  ModelSelectorTrigger,\n  ModelSelectorValue,\n  ModelSelectorContent,\n  ModelSelectorSearch,\n  ModelSelectorFocusAnchor,\n  ModelSelectorList,\n  ModelSelectorEmpty,\n  ModelSelectorGroup,\n  ModelSelectorSeparator,\n  ModelSelectorItem,\n  ModelSelectorEffort,\n};\n"
    }
  ]
}