{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "combobox-rich",
  "title": "Rich combobox",
  "description": "Styled combobox: the Rich set. 5 decorative combobox variations (AvatarCombobox, DescriptionCombobox, BadgeCombobox, MediaCombobox, GroupCombobox) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/combobox-rich.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronsUpDown, Search } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Rich combobox family: 5 decorative SELF-CONTAINED searchable selects. NO radix,\n   NO portal, NO cmdk. This family enriches the ROW ANATOMY: an initial-based avatar,\n   a two-line description, a badge, a token-surfaced media square, and a grouped list.\n   NO REMOTE IMAGE URLs - everything is a token surface, an initial or an inline SVG.\n   Each export is a complete combobox: a real <input> carrying role=\"combobox\"\n   filters, the arrow keys move the highlight, Enter selects, Escape closes. Color\n   comes ONLY from tokens, via alpha color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst triggerHeight: Record<StyledSize, string> = {\n  sm: \"h-8\",\n  md: \"h-9\",\n  lg: \"h-10\",\n  xl: \"h-12\",\n}\n\nconst panelWidth: Record<StyledSize, string> = {\n  sm: \"w-56\",\n  md: \"w-64\",\n  lg: \"w-72\",\n  xl: \"w-80\",\n}\n\nexport interface ComboboxOption {\n  value: string\n  label: React.ReactNode\n}\n\ninterface ComboboxProps {\n  className?: string\n  size?: StyledSize\n  placeholder?: string\n  options?: ComboboxOption[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (v: string) => void\n}\n\nconst defaultOptions: ComboboxOption[] = [\n  { value: \"next\", label: \"Next.js\" },\n  { value: \"react\", label: \"React\" },\n  { value: \"vue\", label: \"Vue\" },\n  { value: \"svelte\", label: \"Svelte\" },\n  { value: \"solid\", label: \"Solid\" },\n  { value: \"astro\", label: \"Astro\" },\n]\n\n/* Row enrichments: a sensible fallback is produced for an unknown value. */\ninterface OptionMeta {\n  description: string\n  badge: string\n  group: string\n}\n\nconst optionMeta: Record<string, OptionMeta> = {\n  next: { description: \"Full stack React framework\", badge: \"SSR\", group: \"Frameworks\" },\n  react: { description: \"The component library\", badge: \"Core\", group: \"Libraries\" },\n  vue: { description: \"Progressive framework\", badge: \"SFC\", group: \"Frameworks\" },\n  svelte: { description: \"Compiled components\", badge: \"Compiler\", group: \"Frameworks\" },\n  solid: { description: \"Fine grained reactivity\", badge: \"Signals\", group: \"Libraries\" },\n  astro: { description: \"Content first sites\", badge: \"Islands\", group: \"Frameworks\" },\n}\n\nfunction metaFor(value: string): OptionMeta {\n  return optionMeta[value] ?? { description: \"Available option\", badge: \"Item\", group: \"Other\" }\n}\n\nfunction nodeText(node: React.ReactNode): string {\n  if (node === null || node === undefined || typeof node === \"boolean\") return \"\"\n  if (typeof node === \"string\" || typeof node === \"number\") return String(node)\n  if (Array.isArray(node)) return node.map(nodeText).join(\"\")\n  if (React.isValidElement(node)) {\n    return nodeText((node.props as { children?: React.ReactNode }).children)\n  }\n  return \"\"\n}\n\n/* An initial from the label: deterministic avatar content instead of a remote image. */\nfunction initials(label: React.ReactNode): string {\n  const text = nodeText(label).trim()\n  if (text.length === 0) return \"?\"\n  const words = text.split(/[\\s.\\-_]+/).filter(Boolean)\n  if (words.length === 1) return words[0].slice(0, 2).toUpperCase()\n  return (words[0][0] + words[1][0]).toUpperCase()\n}\n\nfunction filterOptions(options: ComboboxOption[], query: string): ComboboxOption[] {\n  const q = query.trim().toLowerCase()\n  if (q.length === 0) return options\n  return options.filter((o) => nodeText(o.label).toLowerCase().includes(q))\n}\n\nconst rootBase = \"relative\"\n\nconst inputBase =\n  \"w-full rounded-lg border border-field-border bg-transparent pl-9 pr-9 text-sm text-foreground shadow-xs outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40\"\n\nconst panelBase =\n  \"absolute left-0 right-0 top-full z-50 mt-2 min-w-64 origin-top overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-none\"\n\nconst listBase = \"max-h-72 overflow-y-auto p-1.5\"\n\nconst optionBase =\n  \"flex cursor-pointer select-none items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:shrink-0 [&_i]:leading-none\"\n\nconst emptyBase = \"py-6 text-center text-sm text-muted-foreground\"\n\nconst groupLabel =\n  \"px-2.5 pb-1 pt-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\"\n\nconst panelMotion = {\n  initial: { opacity: 0, scale: 0.98, y: -6 },\n  animate: { opacity: 1, scale: 1, y: 0 },\n  exit: { opacity: 0, scale: 0.98, y: -6 },\n}\n\nfunction useCombobox(props: ComboboxProps) {\n  const { options = defaultOptions, value, defaultValue, onValueChange } = props\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState<string | undefined>(defaultValue)\n  const current = isControlled ? value : internal\n\n  const [open, setOpen] = React.useState(false)\n  const [editing, setEditing] = React.useState(false)\n  const [query, setQuery] = React.useState(\"\")\n  const [active, setActive] = React.useState(0)\n\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n  const uid = React.useId()\n  const listId = `${uid}-list`\n  const optionId = React.useCallback((i: number) => `${uid}-option-${i}`, [uid])\n\n  const selected = React.useMemo(\n    () => (current === undefined ? undefined : options.find((o) => o.value === current)),\n    [options, current]\n  )\n\n  const filtered = React.useMemo(\n    () => (editing ? filterOptions(options, query) : options),\n    [editing, options, query]\n  )\n\n  React.useEffect(() => {\n    setActive((prev) => (prev >= filtered.length ? 0 : prev))\n  }, [filtered.length])\n\n  const close = React.useCallback(() => {\n    setOpen(false)\n    setEditing(false)\n    setQuery(\"\")\n    setActive(0)\n  }, [])\n\n  React.useEffect(() => {\n    if (!open) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") close()\n    }\n    const onPointer = (e: PointerEvent) => {\n      const node = wrapperRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) close()\n    }\n    window.addEventListener(\"keydown\", onKey)\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => {\n      window.removeEventListener(\"keydown\", onKey)\n      window.removeEventListener(\"pointerdown\", onPointer)\n    }\n  }, [open, close])\n\n  const commit = React.useCallback(\n    (next: string) => {\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n      close()\n    },\n    [isControlled, onValueChange, close]\n  )\n\n  const onInputKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"ArrowDown\") {\n        e.preventDefault()\n        setOpen(true)\n        setActive((prev) => (filtered.length === 0 ? 0 : (prev + 1) % filtered.length))\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault()\n        setOpen(true)\n        setActive((prev) =>\n          filtered.length === 0 ? 0 : (prev - 1 + filtered.length) % filtered.length\n        )\n      } else if (e.key === \"Enter\") {\n        e.preventDefault()\n        const item = filtered[active]\n        if (item) commit(item.value)\n      } else if (e.key === \"Escape\") {\n        e.preventDefault()\n        close()\n      }\n    },\n    [filtered, active, commit, close]\n  )\n\n  const inputValue = editing ? query : selected ? nodeText(selected.label) : \"\"\n\n  return {\n    open,\n    setOpen,\n    setEditing,\n    setQuery,\n    active,\n    setActive,\n    filtered,\n    current,\n    commit,\n    onInputKeyDown,\n    wrapperRef,\n    inputValue,\n    listId,\n    optionId,\n    selected,\n  }\n}\n\ntype RichState = ReturnType<typeof useCombobox>\n\n/* Sunum kabugu: input + panel. Panel govdesi children ile gelir. */\nfunction RichField({\n  state,\n  size,\n  className,\n  placeholder,\n  leading,\n  children,\n}: {\n  state: RichState\n  size: StyledSize\n  className?: string\n  placeholder: string\n  leading?: React.ReactNode\n  children: React.ReactNode\n}) {\n  const reduce = useReducedMotion() ?? false\n  const {\n    open,\n    setOpen,\n    setEditing,\n    setQuery,\n    active,\n    filtered,\n    onInputKeyDown,\n    wrapperRef,\n    inputValue,\n    listId,\n    optionId,\n  } = state\n\n  const activeId = open && filtered[active] ? optionId(active) : undefined\n\n  return (\n    <div\n      ref={wrapperRef}\n      data-slot=\"styled-combobox\"\n      className={cn(rootBase, panelWidth[size], className)}\n    >\n      <div className=\"relative\">\n        <span className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground [&_svg]:size-4 [&_i]:text-base [&_i]:leading-none\">\n          {leading ?? <Search />}\n        </span>\n        <input\n          role=\"combobox\"\n          aria-expanded={open}\n          aria-controls={listId}\n          aria-autocomplete=\"list\"\n          aria-activedescendant={activeId}\n          value={inputValue}\n          placeholder={placeholder}\n          onFocus={() => {\n            setOpen(true)\n            setEditing(false)\n          }}\n          onChange={(e) => {\n            setOpen(true)\n            setEditing(true)\n            setQuery(e.target.value)\n          }}\n          onKeyDown={onInputKeyDown}\n          className={cn(inputBase, triggerHeight[size])}\n        />\n        <ChevronsUpDown className=\"pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground opacity-70\" />\n      </div>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            id={listId}\n            role=\"listbox\"\n            className={panelBase}\n            initial={reduce ? { opacity: 0 } : panelMotion.initial}\n            animate={reduce ? { opacity: 1 } : panelMotion.animate}\n            exit={reduce ? { opacity: 0 } : panelMotion.exit}\n            transition={reduce ? { duration: 0 } : { duration: 0.16, ease: \"easeOut\" }}\n          >\n            {children}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* A single rich row; its content arrives through a render prop. */\nfunction RichRow({\n  state,\n  index,\n  option,\n  children,\n}: {\n  state: RichState\n  index: number\n  option: ComboboxOption\n  children: React.ReactNode\n}) {\n  const { active, setActive, current, commit, optionId } = state\n  const isActive = index === active\n  const isSelected = option.value === current\n  return (\n    <div\n      id={optionId(index)}\n      role=\"option\"\n      tabIndex={-1}\n      aria-selected={isSelected}\n      onMouseEnter={() => setActive(index)}\n      onClick={() => commit(option.value)}\n      className={cn(optionBase, (isActive || isSelected) && \"bg-accent text-accent-foreground\")}\n    >\n      {children}\n      {isSelected ? <Check className=\"size-4 text-foreground\" /> : null}\n    </div>\n  )\n}\n\n/* AvatarCombobox: a circle with a token surface and an initial on every row. No\n   remote images. */\nexport function AvatarCombobox(props: ComboboxProps) {\n  const { className, size = \"md\", placeholder = \"Search framework...\" } = props\n  const state = useCombobox(props)\n  const { filtered } = state\n\n  return (\n    <RichField state={state} size={size} className={className} placeholder={placeholder}>\n      <div className={listBase}>\n        {filtered.length === 0 ? (\n          <p className={emptyBase}>No results found.</p>\n        ) : (\n          filtered.map((option, i) => (\n            <RichRow key={option.value} state={state} index={i} option={option}>\n              <span className=\"flex size-7 shrink-0 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-primary)_16%,transparent)] text-[11px] font-semibold text-foreground\">\n                {initials(option.label)}\n              </span>\n              <span className=\"flex-1 truncate\">{option.label}</span>\n            </RichRow>\n          ))\n        )}\n      </div>\n    </RichField>\n  )\n}\n\n/* DescriptionCombobox: a two-line row; title plus a secondary description. */\nexport function DescriptionCombobox(props: ComboboxProps) {\n  const { className, size = \"md\", placeholder = \"Search framework...\" } = props\n  const state = useCombobox(props)\n  const { filtered } = state\n\n  return (\n    <RichField state={state} size={size} className={className} placeholder={placeholder}>\n      <div className={listBase}>\n        {filtered.length === 0 ? (\n          <p className={emptyBase}>No results found.</p>\n        ) : (\n          filtered.map((option, i) => (\n            <RichRow key={option.value} state={state} index={i} option={option}>\n              <span className=\"flex min-w-0 flex-1 flex-col\">\n                <span className=\"truncate font-medium\">{option.label}</span>\n                <span className=\"truncate text-xs text-muted-foreground\">\n                  {metaFor(option.value).description}\n                </span>\n              </span>\n            </RichRow>\n          ))\n        )}\n      </div>\n    </RichField>\n  )\n}\n\n/* BadgeCombobox: satirin sagina token bir rozet asilir. */\nexport function BadgeCombobox(props: ComboboxProps) {\n  const { className, size = \"md\", placeholder = \"Search framework...\" } = props\n  const state = useCombobox(props)\n  const { filtered } = state\n\n  return (\n    <RichField state={state} size={size} className={className} placeholder={placeholder}>\n      <div className={listBase}>\n        {filtered.length === 0 ? (\n          <p className={emptyBase}>No results found.</p>\n        ) : (\n          filtered.map((option, i) => (\n            <RichRow key={option.value} state={state} index={i} option={option}>\n              <span className=\"flex-1 truncate\">{option.label}</span>\n              <span className=\"shrink-0 rounded-md border border-border bg-secondary px-1.5 py-0.5 text-[11px] font-medium text-secondary-foreground\">\n                {metaFor(option.value).badge}\n              </span>\n            </RichRow>\n          ))\n        )}\n      </div>\n    </RichField>\n  )\n}\n\n/* MediaCombobox: a square media card on a token surface carrying an inline SVG. */\nexport function MediaCombobox(props: ComboboxProps) {\n  const { className, size = \"md\", placeholder = \"Search framework...\" } = props\n  const state = useCombobox(props)\n  const { filtered } = state\n\n  return (\n    <RichField state={state} size={size} className={className} placeholder={placeholder}>\n      <div className={listBase}>\n        {filtered.length === 0 ? (\n          <p className={emptyBase}>No results found.</p>\n        ) : (\n          filtered.map((option, i) => (\n            <RichRow key={option.value} state={state} index={i} option={option}>\n              <span className=\"flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-md border border-border bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)]\">\n                <svg\n                  viewBox=\"0 0 24 24\"\n                  aria-hidden=\"true\"\n                  className=\"size-5 text-muted-foreground\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"1.5\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                >\n                  <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"2\" />\n                  <path d=\"m3 15 5-5 4 4 3-3 6 6\" />\n                  <circle cx=\"8.5\" cy=\"8.5\" r=\"1.25\" />\n                </svg>\n              </span>\n              <span className=\"flex min-w-0 flex-1 flex-col\">\n                <span className=\"truncate font-medium\">{option.label}</span>\n                <span className=\"truncate text-xs text-muted-foreground\">\n                  {metaFor(option.value).description}\n                </span>\n              </span>\n            </RichRow>\n          ))\n        )}\n      </div>\n    </RichField>\n  )\n}\n\n/* GroupCombobox: the rows are clustered under headings. The filter preserves the flat index, so the arrow keys move across groups without interruption. */\nexport function GroupCombobox(props: ComboboxProps) {\n  const { className, size = \"md\", placeholder = \"Search framework...\" } = props\n  const state = useCombobox(props)\n  const { filtered } = state\n\n  /* Group while preserving the flat index. */\n  const groups = React.useMemo(() => {\n    const out: { name: string; items: { option: ComboboxOption; index: number }[] }[] = []\n    filtered.forEach((option, index) => {\n      const name = metaFor(option.value).group\n      const bucket = out.find((g) => g.name === name)\n      if (bucket) bucket.items.push({ option, index })\n      else out.push({ name, items: [{ option, index }] })\n    })\n    return out\n  }, [filtered])\n\n  return (\n    <RichField state={state} size={size} className={className} placeholder={placeholder}>\n      <div className={listBase}>\n        {filtered.length === 0 ? (\n          <p className={emptyBase}>No results found.</p>\n        ) : (\n          groups.map((group) => (\n            <div key={group.name} role=\"group\" aria-label={group.name}>\n              <p className={groupLabel}>{group.name}</p>\n              {group.items.map(({ option, index }) => (\n                <RichRow key={option.value} state={state} index={index} option={option}>\n                  <span className=\"flex-1 truncate\">{option.label}</span>\n                </RichRow>\n              ))}\n            </div>\n          ))\n        )}\n      </div>\n    </RichField>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/combobox-rich.tsx"
    }
  ],
  "categories": [
    "styled",
    "combobox"
  ],
  "type": "registry:component"
}