{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "components-ai-model-selector",
  "title": "Model Selector",
  "description": "A modal-based AI model picker with provider filtering, search, starred models, and capability badges.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "dialog",
    "tooltip",
    "@odysseyui/components-texts-text-shimmer"
  ],
  "files": [
    {
      "path": "registry/components/ai/model-selector/index.tsx",
      "content": "'use client';\n\nimport {\n  useState,\n  useContext,\n  createContext,\n  type ReactNode,\n  type Dispatch,\n  type SetStateAction,\n} from 'react';\nimport { motion, AnimatePresence } from 'motion/react';\nimport {\n  Search,\n  X,\n  ChevronDown,\n  Eye,\n  Brain,\n  Globe,\n  Star,\n  LayoutGrid,\n  ArrowRight,\n} from 'lucide-react';\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogClose,\n} from '@/components/ui/dialog';\nimport { cn } from '@/lib/utils';\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from '@/components/ui/tooltip';\nimport { ShimmerText } from '@/components/odyssey/components/texts/text-shimmer';\n\nexport type Cap = 'vision' | 'tools' | 'search';\nexport type Cost = string;\n\nexport interface Provider {\n  id: string;\n  icon: ReactNode;\n  label: string;\n}\n\nexport interface Model {\n  id: string;\n  provider: string;\n  name: string;\n  desc: string;\n  cost: Cost;\n  tag: string | null;\n  caps: string[];\n  starred: boolean;\n}\n\ninterface ModelSelectorCtxValue {\n  providers: Provider[];\n  models: Model[];\n  open: boolean;\n  setOpen: Dispatch<SetStateAction<boolean>>;\n  selected: Model;\n  setSelected: Dispatch<SetStateAction<Model>>;\n  search: string;\n  setSearch: Dispatch<SetStateAction<string>>;\n  activeProvider: string | null;\n  setActiveProvider: Dispatch<SetStateAction<string | null>>;\n  filtered: Model[];\n  starred: Set<string>;\n  toggleStar: (id: string) => void;\n}\n\nconst ModelSelectorCtx = createContext<ModelSelectorCtxValue | null>(null);\n\nconst useModelSelector = () => {\n  const ctx = useContext(ModelSelectorCtx);\n  if (!ctx) throw new Error('Must be used inside <ModelSelector>');\n  return ctx;\n};\n\nconst CAP_ICONS: Record<\n  Cap,\n  { icon: React.ElementType; label: string; color: string }\n> = {\n  vision: { icon: Eye, label: 'Vision', color: 'text-violet-400' },\n  tools: { icon: Brain, label: 'Reasoning', color: 'text-orange-400' },\n  search: { icon: Globe, label: 'Search', color: 'text-emerald-400' },\n};\n\nconst COST_CLASS = 'text-muted-foreground/50 font-mono text-xs';\n\nexport function ModelSelector({\n  children,\n  providers,\n  models,\n  defaultModel,\n}: {\n  children: ReactNode;\n  providers: Provider[];\n  models: Model[];\n  defaultModel?: Model;\n}) {\n  const initialModel = defaultModel ?? models[0];\n  const [open, setOpen] = useState(false);\n  const [selected, setSelected] = useState<Model>(initialModel);\n  const [search, setSearch] = useState('');\n  const [activeProvider, setActiveProvider] = useState<string | null>(null);\n  const [starred, setStarred] = useState(\n    () => new Set(models.filter((m) => m.starred).map((m) => m.id)),\n  );\n\n  const toggleStar = (id: string) =>\n    setStarred((prev) => {\n      const n = new Set(prev);\n      if (n.has(id)) {\n        n.delete(id);\n      } else {\n        n.add(id);\n      }\n      return n;\n    });\n\n  const filtered = models.filter((m) => {\n    const matchProvider = !activeProvider || m.provider === activeProvider;\n    const q = search.toLowerCase();\n    const matchSearch =\n      !q ||\n      m.name.toLowerCase().includes(q) ||\n      m.desc.toLowerCase().includes(q);\n    return matchProvider && matchSearch;\n  });\n\n  return (\n    <ModelSelectorCtx.Provider\n      value={{\n        providers,\n        models,\n        open,\n        setOpen,\n        selected,\n        setSelected,\n        search,\n        setSearch,\n        activeProvider,\n        setActiveProvider,\n        filtered,\n        starred,\n        toggleStar,\n      }}\n    >\n      {children}\n    </ModelSelectorCtx.Provider>\n  );\n}\n\nexport function ModelSelectorTrigger({\n  className = '',\n}: {\n  className?: string;\n}) {\n  const { setOpen, selected } = useModelSelector();\n  return (\n    <motion.button\n      onClick={() => setOpen(true)}\n      whileHover={{ scale: 1.02 }}\n      whileTap={{ scale: 0.97 }}\n      className={`flex items-center gap-2 px-3 py-1.5 rounded-xl bg-secondary border border-border text-sm font-medium text-secondary-foreground hover:bg-accent hover:text-accent-foreground transition-colors ${className}`}\n    >\n      <span className=\"text-xs text-muted-foreground font-mono\">\n        <ShimmerText text={selected.name} />\n      </span>\n      <span className={COST_CLASS}>{selected.cost}</span>\n      <ChevronDown className=\"w-3 h-3 text-muted-foreground/60\" />\n    </motion.button>\n  );\n}\n\nexport function ModelSelectorModal() {\n  const { open, setOpen } = useModelSelector();\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent\n        showCloseButton={false}\n        aria-describedby={undefined}\n        className={cn(\n          'flex flex-col gap-0 p-0 ring-0 border border-border shadow-2xl overflow-hidden duration-200',\n          'top-auto bottom-0 inset-x-0 translate-x-0 translate-y-0 max-w-full rounded-t-3xl rounded-b-none max-h-[85vh]',\n          'sm:top-1/2 sm:left-1/2 sm:right-auto sm:bottom-auto sm:w-130 sm:max-w-130 sm:-translate-x-1/2 sm:-translate-y-1/2 sm:rounded-2xl sm:max-h-150',\n        )}\n      >\n        <DialogTitle className=\"sr-only\">Select a model</DialogTitle>\n        <ModelSelectorHeader />\n        <div className=\"flex flex-1 min-h-0\">\n          <ModelSelectorProviderSidebar />\n          <ModelSelectorModelList />\n        </div>\n        <ModelSelectorFooter />\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nfunction ModelSelectorHeader() {\n  const { search, setSearch } = useModelSelector();\n  return (\n    <div className=\"flex items-center gap-3 px-4 pt-4 pb-3 border-b border-border\">\n      <Search className=\"w-4 h-4 text-muted-foreground shrink-0\" />\n      <input\n        autoFocus\n        value={search}\n        onChange={(e) => setSearch(e.target.value)}\n        placeholder=\"Search models…\"\n        className=\"flex-1 bg-transparent text-sm text-popover-foreground placeholder:text-muted-foreground/50 outline-none font-light tracking-wide font-mono\"\n      />\n      <AnimatePresence>\n        {search && (\n          <motion.button\n            initial={{ scale: 0, opacity: 0 }}\n            animate={{ scale: 1, opacity: 1 }}\n            exit={{ scale: 0, opacity: 0 }}\n            transition={{ duration: 0.15 }}\n            onClick={() => setSearch('')}\n            className=\"text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            <X className=\"w-3.5 h-3.5\" />\n          </motion.button>\n        )}\n      </AnimatePresence>\n      <DialogClose asChild>\n        <button className=\"ml-1 w-6 h-6 rounded-lg flex items-center justify-center bg-secondary hover:bg-accent text-muted-foreground hover:text-accent-foreground transition-colors\">\n          <X className=\"w-3.5 h-3.5\" />\n        </button>\n      </DialogClose>\n    </div>\n  );\n}\n\nfunction ModelSelectorProviderSidebar() {\n  const { providers, activeProvider, setActiveProvider } = useModelSelector();\n  return (\n    <div className=\"flex flex-col gap-0.5 py-3 px-2 border-r border-border w-14 shrink-0\">\n      <SidebarBtn\n        active={!activeProvider}\n        onClick={() => setActiveProvider(null)}\n        title=\"All\"\n      >\n        <LayoutGrid className=\"w-4 h-4\" />\n      </SidebarBtn>\n      {providers.map((p) => (\n        <SidebarBtn\n          key={p.id}\n          active={activeProvider === p.id}\n          onClick={() =>\n            setActiveProvider(activeProvider === p.id ? null : p.id)\n          }\n          title={p.label}\n        >\n          <span className=\"w-4 h-4 flex items-center justify-center [&>svg]:w-full [&>svg]:h-full\">\n            {p.icon}\n          </span>\n        </SidebarBtn>\n      ))}\n    </div>\n  );\n}\n\nfunction SidebarBtn({\n  active,\n  onClick,\n  title,\n  children,\n}: {\n  active: boolean;\n  onClick: () => void;\n  title: string;\n  children: ReactNode;\n}) {\n  return (\n    <motion.button\n      onClick={onClick}\n      whileHover={{ scale: 1.08 }}\n      whileTap={{ scale: 0.92 }}\n      title={title}\n      className={`relative w-9 h-9 rounded-xl flex items-center justify-center transition-colors mx-auto ${\n        active\n          ? 'bg-accent text-accent-foreground'\n          : 'text-muted-foreground hover:text-foreground hover:bg-accent/50'\n      }`}\n    >\n      {active && (\n        <motion.div\n          layoutId=\"provider-indicator\"\n          className=\"absolute inset-0 rounded-xl bg-accent border border-border\"\n          transition={{ type: 'spring', stiffness: 350, damping: 30 }}\n        />\n      )}\n      <span className=\"relative z-10\">{children}</span>\n    </motion.button>\n  );\n}\n\nfunction ModelSelectorModelList() {\n  const { filtered } = useModelSelector();\n  return (\n    <div\n      className=\"flex-1 overflow-y-auto py-2 px-1 space-y-0.5\"\n      style={{ scrollbarWidth: 'none' }}\n    >\n      {filtered.length === 0 && (\n        <div className=\"py-12 text-center text-muted-foreground/60 text-sm font-mono\">\n          No models found\n        </div>\n      )}\n      {filtered.map((model, i) => (\n        <ModelSelectorModelRow key={model.id} model={model} index={i} />\n      ))}\n    </div>\n  );\n}\n\nfunction ModelSelectorModelRow({\n  model,\n  index,\n}: {\n  model: Model;\n  index: number;\n}) {\n  const { providers, selected, setSelected, setOpen, starred, toggleStar } =\n    useModelSelector();\n  const isSelected = selected.id === model.id;\n  const isStarred = starred.has(model.id);\n\n  return (\n    <motion.div\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 1 }}\n      exit={{ opacity: 0 }}\n      transition={{ delay: index * 0.025, duration: 0.2 }}\n    >\n      <button\n        onClick={() => {\n          setSelected(model);\n          setOpen(false);\n        }}\n        className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-left transition-colors group border hover:bg-accent ${\n          isSelected ? 'bg-accent border-border' : 'border-transparent'\n        }`}\n      >\n        <div className=\"w-8 h-8 rounded-lg shrink-0 flex items-center justify-center text-sm font-bold bg-secondary border border-border\">\n          <span className=\"w-4 h-4 flex items-center justify-center [&>svg]:w-full [&>svg]:h-full\">\n            {providers.find((p) => p.id === model.provider)?.icon ?? '◈'}\n          </span>\n        </div>\n\n        <div className=\"flex-1 min-w-0\">\n          <div className=\"flex items-center gap-2\">\n            <span className=\"text-sm font-semibold text-foreground truncate font-mono tracking-tight\">\n              {model.name}\n            </span>\n            {model.tag && (\n              <span className=\"px-1.5 py-0.5 rounded-md text-[10px] font-bold tracking-wide bg-primary/10 text-primary border border-primary/20\">\n                {model.tag}\n              </span>\n            )}\n          </div>\n          <p className=\"text-xs text-muted-foreground truncate mt-0.5 leading-snug\">\n            {model.desc}\n          </p>\n        </div>\n\n        <div className=\"flex items-center gap-1 shrink-0\">\n          {model.caps.length > 0 && (\n            <div className=\"flex items-center gap-1 px-1.5 py-1 rounded-lg bg-secondary border border-border/60\">\n              <TooltipProvider>\n                {model.caps.map((c) => {\n                  const entry = CAP_ICONS[c as Cap];\n                  if (!entry) return null;\n                  const Icon = entry.icon;\n                  return (\n                    <Tooltip key={c}>\n                      <TooltipTrigger asChild>\n                        <span className={entry.color}>\n                          <Icon className=\"w-3 h-3\" />\n                        </span>\n                      </TooltipTrigger>\n                      <TooltipContent side=\"top\">{entry.label}</TooltipContent>\n                    </Tooltip>\n                  );\n                })}\n              </TooltipProvider>\n            </div>\n          )}\n\n          <motion.div\n            role=\"button\"\n            tabIndex={0}\n            aria-label={isStarred ? 'Unstar model' : 'Star model'}\n            onClick={(e) => {\n              e.stopPropagation();\n              toggleStar(model.id);\n            }}\n            onKeyDown={(e) => {\n              if (e.key === 'Enter' || e.key === ' ') {\n                e.preventDefault();\n                e.stopPropagation();\n                toggleStar(model.id);\n              }\n            }}\n            whileHover={{ scale: 1.2 }}\n            whileTap={{ scale: 0.85 }}\n            className=\"ml-1 w-5 h-5 flex items-center justify-center cursor-pointer\"\n          >\n            <Star\n              className={`w-3.5 h-3.5 transition-colors ${\n                isStarred\n                  ? 'fill-yellow-400 text-yellow-400'\n                  : 'text-muted-foreground/30 hover:text-muted-foreground'\n              }`}\n              stroke={isStarred ? '#FF8904' : '#6A7282'}\n            />\n          </motion.div>\n        </div>\n      </button>\n    </motion.div>\n  );\n}\n\nfunction ModelSelectorFooter() {\n  const { selected } = useModelSelector();\n  return (\n    <div className=\"px-4 py-3 border-t border-border flex items-center justify-between\">\n      <div className=\"flex items-center gap-2\">\n        <span className=\"text-xs text-muted-foreground font-mono\">\n          {selected.name}\n        </span>\n      </div>\n      <a\n        href=\"#\"\n        className=\"flex items-center gap-1 text-xs text-muted-foreground/50 hover:text-muted-foreground transition-colors font-mono\"\n      >\n        Upgrade for more\n        <ArrowRight className=\"w-3 h-3\" />\n      </a>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/odysseyui/model-selector.tsx"
    }
  ],
  "type": "registry:ui"
}