{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-model-selection",
  "type": "registry:component",
  "title": "Elements Model Selection",
  "description": "Shared model selection state and effort resolution for model selectors.",
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/utils/model-selection.ts",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/utils/model-selection.ts",
      "content": "\"use client\";\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\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\nexport function 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\nexport function 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\nexport function 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  open: boolean;\n  setOpen: (open: boolean) => void;\n};\n\nexport const ModelSelectorContext =\n  createContext<ModelSelectorContextValue | null>(null);\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\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. Base UI only exposes lazy flipping on the Combobox\n// positioner, so adopt the side the popup first flips to as the preferred side\n// for the rest of the open. Adopting every change can alternate between sides\n// without end: after a preference change the popup can render on the opposite\n// side even when both sides fit.\n//\n// A force-mounted popup keeps its node and position while closed. Pass `open`\n// and the `preferred` side for one, so the side resets on close and a reopen\n// adopts a side the popup already flipped to.\nexport function useLazyFlipSide<TSide extends string>(\n  open = true,\n  preferred?: TSide,\n): {\n  side: TSide | undefined;\n  popupRef: (node: HTMLDivElement | null) => void;\n} {\n  const [side, setSide] = useState<TSide | undefined>();\n  const observerRef = useRef<MutationObserver | null>(null);\n  const preferredRef = useRef(preferred);\n  preferredRef.current = preferred;\n  const popupRef = useCallback(\n    (node: HTMLDivElement | null) => {\n      observerRef.current?.disconnect();\n      observerRef.current = null;\n      if (!node || !open) {\n        setSide(undefined);\n        return;\n      }\n      const current = node.getAttribute(\"data-side\");\n      const hasFlipped =\n        preferredRef.current !== undefined &&\n        current !== null &&\n        current !== preferredRef.current;\n      if (hasFlipped) {\n        setSide(current as TSide);\n        return;\n      }\n      const observer = new MutationObserver(() => {\n        const rendered = node.getAttribute(\"data-side\");\n        if (!rendered) return;\n        observer.disconnect();\n        setSide(rendered as TSide);\n      });\n      observer.observe(node, {\n        attributes: true,\n        attributeFilter: [\"data-side\"],\n      });\n      observerRef.current = observer;\n    },\n    [open],\n  );\n  return { side, popupRef };\n}\n"
    }
  ]
}