{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-input",
  "type": "registry:ui",
  "registryDependencies": ["b-switch"],
  "dependencies": ["@base-ui/react", "motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "ai-input.tsx",
      "content": "\"use client\";\n\nimport { Input as InputPrimitive } from \"@base-ui/react/input\";\nimport {\n  ArrowUp,\n  Check,\n  ChevronDown,\n  ChevronLeft,\n  ChevronRight,\n  Mic,\n  Plus,\n} from \"lucide-react\";\nimport {\n  AnimatePresence,\n  LayoutGroup,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  type ComponentType,\n  Fragment,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal, flushSync } from \"react-dom\";\n\nimport { Switch } from \"@/components/ui/b-switch\";\n\nconst WAVE_DURATION_MS = 1000;\nconst MAX_TEXTAREA_HEIGHT = 132;\n\n/** Spring used by the shared hover highlight that glides between plus-menu rows. */\nconst HOVER_SPRING = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 34,\n  mass: 0.58,\n} as const;\n\n/** Apple Intelligence-style spectrum used by the send wave. */\nconst WAVE_WASH_GRADIENT =\n  \"linear-gradient(180deg, transparent, rgba(34,211,238,0.07), rgba(59,130,246,0.09), rgba(217,70,239,0.1), rgba(244,63,94,0.09), rgba(249,115,22,0.08), transparent)\";\n\nexport type AIInputOption = {\n  value: string;\n  label: string;\n};\n\nexport type AIInputMessage = {\n  id: number;\n  text: string;\n};\n\n// ─── Settings dropdown types ────────────────────────────────────────────────\n\nexport type PromptSettingOption = {\n  value: string;\n  label: string;\n  description?: string;\n};\n\nexport type PromptSettingGroup = {\n  id: string;\n  label: string;\n  options: PromptSettingOption[];\n  /** Show the selected option as a featured row with description and checkmark. */\n  display?: \"featured\" | \"submenu\";\n  /** Optional submenu row label for picking other options (e.g. \"More models\"). */\n  moreMenuLabel?: string;\n};\n\nexport type PromptMenuAction = {\n  label: string;\n  icon?: ReactNode;\n  onSelect: () => void;\n};\n\n// ─── Settings dropdown helpers ───────────────────────────────────────────────\n\nfunction getDefaultSettings(\n  groups: PromptSettingGroup[],\n  defaults?: Record<string, string>\n) {\n  return groups.reduce<Record<string, string>>((acc, group) => {\n    const preferred = defaults?.[group.id];\n    const hasPreferred = group.options.some((opt) => opt.value === preferred);\n    acc[group.id] =\n      hasPreferred && preferred ? preferred : (group.options[0]?.value ?? \"\");\n    return acc;\n  }, {});\n}\n\nfunction getOptionLabel(group: PromptSettingGroup, value: string) {\n  return group.options.find((option) => option.value === value)?.label ?? value;\n}\n\nfunction getSelectedOption(group: PromptSettingGroup, value: string) {\n  return group.options.find((option) => option.value === value);\n}\n\nconst dropdownPanelClassName =\n  \"fixed z-[400] w-[min(14.5rem,calc(100vw-1.5rem))] overflow-y-auto overscroll-contain rounded-xl border border-border/60 bg-card py-1 text-sm shadow-[0_10px_28px_-20px_rgba(0,0,0,0.14)] outline-none [-webkit-overflow-scrolling:touch]\";\n\nconst DROPDOWN_SIDE_OFFSET = 8;\nconst DROPDOWN_VIEWPORT_MARGIN = 12;\nconst DROPDOWN_SUBMENU_OFFSET = 4;\nconst DROPDOWN_PANEL_WIDTH = 232;\nconst DROPDOWN_ESTIMATED_HEIGHT = 240;\nconst COMPACT_VIEWPORT_WIDTH = 640;\nconst DROPDOWN_MIN_PANEL_HEIGHT = 160;\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.min(Math.max(value, min), max);\n}\n\nfunction isCompactViewport() {\n  return window.innerWidth < COMPACT_VIEWPORT_WIDTH;\n}\n\nfunction getPanelWidth(viewportWidth: number) {\n  return Math.min(\n    DROPDOWN_PANEL_WIDTH,\n    viewportWidth - DROPDOWN_VIEWPORT_MARGIN * 2\n  );\n}\n\nfunction useCompactViewport() {\n  const [isCompact, setIsCompact] = useState(() => {\n    if (typeof window === \"undefined\") return false;\n    return window.matchMedia(`(max-width: ${COMPACT_VIEWPORT_WIDTH - 1}px)`)\n      .matches;\n  });\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\n      `(max-width: ${COMPACT_VIEWPORT_WIDTH - 1}px)`\n    );\n    const update = () => setIsCompact(mediaQuery.matches);\n    update();\n    mediaQuery.addEventListener(\"change\", update);\n    return () => mediaQuery.removeEventListener(\"change\", update);\n  }, []);\n\n  return isCompact;\n}\n\nfunction usePrefersHover() {\n  const [prefersHover, setPrefersHover] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setPrefersHover(mediaQuery.matches);\n    update();\n    mediaQuery.addEventListener(\"change\", update);\n    return () => mediaQuery.removeEventListener(\"change\", update);\n  }, []);\n\n  return prefersHover;\n}\n\nfunction useIsMounted() {\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n  return mounted;\n}\n\nfunction isInsideDropdownPanel(node: Node | null) {\n  return (\n    node instanceof Element &&\n    Boolean(node.closest(\"[data-prompt-dropdown-panel]\"))\n  );\n}\n\nfunction useDropdownDismiss({\n  open,\n  onClose,\n  onEscape,\n  triggerRef,\n}: {\n  open: boolean;\n  onClose: () => void;\n  onEscape?: () => boolean;\n  triggerRef: React.RefObject<HTMLElement | null>;\n}) {\n  useEffect(() => {\n    if (!open) return;\n\n    const handlePointerDown = (event: PointerEvent) => {\n      const target = event.target as Node;\n      if (triggerRef.current?.contains(target)) return;\n      if (isInsideDropdownPanel(target)) return;\n      onClose();\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key !== \"Escape\") return;\n      event.stopPropagation();\n      if (onEscape?.()) return;\n      onClose();\n    };\n\n    document.addEventListener(\"pointerdown\", handlePointerDown);\n    document.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      document.removeEventListener(\"pointerdown\", handlePointerDown);\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [onClose, onEscape, open, triggerRef]);\n}\n\nfunction preventFocusSteal(event: React.MouseEvent) {\n  event.preventDefault();\n}\n\nfunction activateOnEnterOrSpace(\n  event: React.KeyboardEvent,\n  action: () => void\n) {\n  if (event.key === \"Enter\" || event.key === \" \") {\n    event.preventDefault();\n    action();\n  }\n}\n\nfunction ChevronDownIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"10\"\n      viewBox=\"0 0 14 14\"\n      width=\"10\"\n    >\n      <path\n        d=\"M3.5 5.25L7 8.75L10.5 5.25\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"1.5\"\n      />\n    </svg>\n  );\n}\n\nconst BAR_XS = [1.5, 5.75, 10] as const;\nconst BAR_WIDTH = 2.5;\nconst BAR_BOTTOM = 12.5;\n\ntype EffortLevel = \"low\" | \"medium\" | \"high\";\n\nconst BAR_HEIGHTS = [4.5, 7.5, 10.5] as const;\n\nconst EFFORT_BAR_LEVELS: Record<\n  EffortLevel,\n  Array<{ height: number; visible: boolean }>\n> = {\n  low: [\n    { height: 3.25, visible: true },\n    { height: 4.75, visible: true },\n    { height: 0, visible: false },\n  ],\n  medium: [\n    { height: BAR_HEIGHTS[0], visible: true },\n    { height: BAR_HEIGHTS[1], visible: true },\n    { height: 0, visible: false },\n  ],\n  high: [\n    { height: BAR_HEIGHTS[0], visible: true },\n    { height: BAR_HEIGHTS[1], visible: true },\n    { height: BAR_HEIGHTS[2], visible: true },\n  ],\n};\n\nfunction normalizeEffortLevel(value: string): EffortLevel {\n  if (value === \"low\" || value === \"medium\" || value === \"high\") return value;\n  return \"medium\";\n}\n\nfunction getEffortGroupId(groups: PromptSettingGroup[]) {\n  return (\n    groups.find((group) => group.id === \"effort\")?.id ??\n    groups.find((group) => group.display === \"submenu\")?.id\n  );\n}\n\nconst SETTINGS_TRANSITION = {\n  type: \"spring\" as const,\n  stiffness: 380,\n  damping: 34,\n};\n\nfunction EffortBarsIcon({ level }: { level: EffortLevel }) {\n  const bars = EFFORT_BAR_LEVELS[level];\n\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"14\"\n      viewBox=\"0 0 14 14\"\n      width=\"14\"\n    >\n      {bars.map((bar, index) => (\n        <motion.rect\n          animate={{\n            height: bar.visible ? bar.height : 0,\n            opacity: bar.visible ? 1 : 0,\n            y: bar.visible ? BAR_BOTTOM - bar.height : BAR_BOTTOM,\n          }}\n          fill=\"currentColor\"\n          initial={false}\n          key={BAR_XS[index]}\n          rx={1}\n          transition={SETTINGS_TRANSITION}\n          width={BAR_WIDTH}\n          x={BAR_XS[index]}\n        />\n      ))}\n    </svg>\n  );\n}\n\nconst dropdownSubmenuTriggerClassName =\n  \"relative flex min-h-9 w-full cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left text-sm outline-none transition-colors focus-visible:outline-none\";\n\nfunction DropdownFeaturedRow({\n  description,\n  label,\n}: {\n  description?: string;\n  label: string;\n}) {\n  return (\n    <div className=\"flex items-start justify-between gap-2.5 px-2.5 py-2\">\n      <div className=\"min-w-0\">\n        <p className=\"font-medium text-foreground leading-tight\">{label}</p>\n        {description ? (\n          <p className=\"mt-0.5 text-muted-foreground text-xs leading-snug\">\n            {description}\n          </p>\n        ) : null}\n      </div>\n      <Check\n        aria-hidden=\"true\"\n        className=\"mt-0.5 size-4 shrink-0 text-brand\"\n        strokeWidth={2.25}\n      />\n    </div>\n  );\n}\n\nconst dropdownOptionClassName =\n  \"relative flex min-h-9 w-full cursor-pointer scroll-m-1 touch-manipulation items-center justify-between gap-2.5 rounded-lg px-2.5 py-1.5 text-left text-sm outline-none transition-colors focus-visible:text-foreground focus-visible:outline-none\";\n\nconst dropdownOptionHighlightClassName =\n  \"absolute inset-x-1 inset-y-0.5 rounded-lg bg-accent/65\";\n\nconst dropdownHighlightSpring = {\n  type: \"spring\" as const,\n  stiffness: 600,\n  damping: 38,\n};\n\nconst dropdownLabelSpring = {\n  type: \"spring\" as const,\n  stiffness: 360,\n  damping: 28,\n  mass: 0.55,\n};\n\nconst dropdownCheckSpring = {\n  type: \"spring\" as const,\n  stiffness: 460,\n  damping: 24,\n  mass: 0.5,\n};\n\nconst dropdownPanelSpring = {\n  type: \"spring\" as const,\n  stiffness: 420,\n  damping: 34,\n  mass: 0.72,\n};\n\ntype SubmenuOrigin = \"left\" | \"right\" | \"below\";\n\ntype SubmenuPosition = {\n  left: number;\n  top: number;\n  maxHeight: number;\n  origin: SubmenuOrigin;\n};\n\ntype DropdownSide = \"top\" | \"bottom\";\n\ntype DropdownPosition = {\n  left: number;\n  side: DropdownSide;\n  top?: number;\n  bottom?: number;\n  maxHeight: number;\n};\n\nfunction getMainDropdownMotion(side: DropdownSide, instantClose = false) {\n  const offsetY = side === \"bottom\" ? -10 : 10;\n\n  return {\n    animate: { opacity: 1, scale: 1, y: 0 },\n    exit: {\n      opacity: 0,\n      scale: instantClose ? 1 : 0.98,\n      y: instantClose ? 0 : offsetY * 0.65,\n    },\n    initial: { opacity: 0, scale: 0.96, y: offsetY },\n    transition: instantClose ? { duration: 0 } : dropdownPanelSpring,\n  };\n}\n\nfunction getMainDropdownInnerMotion(side: DropdownSide, instantClose = false) {\n  const offsetY = side === \"bottom\" ? 5 : -5;\n\n  return {\n    animate: { opacity: 1, y: 0 },\n    exit: { opacity: 0, y: instantClose ? 0 : offsetY * 0.5 },\n    initial: { opacity: 0, y: offsetY },\n    transition: instantClose\n      ? { duration: 0 }\n      : { duration: 0.22, ease: [0.22, 1, 0.36, 1] as const },\n  };\n}\n\nfunction getFlyoutSubmenuMotion(origin: SubmenuOrigin, instantClose = false) {\n  const offsetX = origin === \"right\" ? -12 : 12;\n\n  return {\n    animate: { opacity: 1, scale: 1, x: 0 },\n    exit: {\n      opacity: 0,\n      scale: instantClose ? 1 : 0.98,\n      x: instantClose ? 0 : offsetX * 0.55,\n    },\n    initial: { opacity: 0, scale: 0.96, x: offsetX },\n    transition: instantClose ? { duration: 0 } : dropdownPanelSpring,\n  };\n}\n\nfunction getSubmenuPosition(\n  triggerRect: DOMRect,\n  panelHeight: number,\n  panelWidth: number\n) {\n  const viewportHeight = window.innerHeight;\n  const viewportWidth = window.innerWidth;\n  const effectiveWidth = Math.min(panelWidth, getPanelWidth(viewportWidth));\n\n  if (isCompactViewport()) {\n    const left = clamp(\n      triggerRect.left,\n      DROPDOWN_VIEWPORT_MARGIN,\n      viewportWidth - effectiveWidth - DROPDOWN_VIEWPORT_MARGIN\n    );\n    const top = triggerRect.bottom + DROPDOWN_SIDE_OFFSET;\n    const maxHeight = Math.max(\n      DROPDOWN_MIN_PANEL_HEIGHT,\n      viewportHeight - top - DROPDOWN_VIEWPORT_MARGIN\n    );\n\n    return { left, maxHeight, origin: \"below\" as const, top };\n  }\n\n  const spaceRight =\n    viewportWidth -\n    triggerRect.right -\n    DROPDOWN_SUBMENU_OFFSET -\n    DROPDOWN_VIEWPORT_MARGIN;\n  const openRight = spaceRight >= effectiveWidth;\n\n  const left = openRight\n    ? triggerRect.right + DROPDOWN_SUBMENU_OFFSET\n    : triggerRect.left - effectiveWidth - DROPDOWN_SUBMENU_OFFSET;\n\n  const clampedLeft = clamp(\n    left,\n    DROPDOWN_VIEWPORT_MARGIN,\n    viewportWidth - effectiveWidth - DROPDOWN_VIEWPORT_MARGIN\n  );\n\n  const maxTop = Math.max(\n    DROPDOWN_VIEWPORT_MARGIN,\n    viewportHeight - panelHeight - DROPDOWN_VIEWPORT_MARGIN\n  );\n  const top = clamp(triggerRect.top, DROPDOWN_VIEWPORT_MARGIN, maxTop);\n  const maxHeight = Math.max(\n    DROPDOWN_MIN_PANEL_HEIGHT,\n    viewportHeight - top - DROPDOWN_VIEWPORT_MARGIN\n  );\n\n  return {\n    left: clampedLeft,\n    maxHeight,\n    origin: openRight ? (\"right\" as const) : (\"left\" as const),\n    top,\n  };\n}\n\nfunction getDropdownPosition({\n  align = \"start\",\n  panelHeight,\n  panelWidth,\n  triggerRect,\n}: {\n  align?: \"end\" | \"start\";\n  panelHeight: number;\n  panelWidth: number;\n  triggerRect: DOMRect;\n}): DropdownPosition {\n  const viewportHeight = window.innerHeight;\n  const viewportWidth = window.innerWidth;\n  const effectiveWidth = Math.min(panelWidth, getPanelWidth(viewportWidth));\n\n  const spaceBelow =\n    viewportHeight -\n    triggerRect.bottom -\n    DROPDOWN_SIDE_OFFSET -\n    DROPDOWN_VIEWPORT_MARGIN;\n  const spaceAbove =\n    triggerRect.top - DROPDOWN_SIDE_OFFSET - DROPDOWN_VIEWPORT_MARGIN;\n\n  const openBelow =\n    spaceBelow >= panelHeight\n      ? true\n      : spaceAbove >= panelHeight\n        ? false\n        : spaceBelow >= spaceAbove;\n\n  const desiredLeft =\n    align === \"end\" ? triggerRect.right - effectiveWidth : triggerRect.left;\n\n  const left = clamp(\n    desiredLeft,\n    DROPDOWN_VIEWPORT_MARGIN,\n    viewportWidth - effectiveWidth - DROPDOWN_VIEWPORT_MARGIN\n  );\n\n  const maxHeight = Math.max(\n    DROPDOWN_MIN_PANEL_HEIGHT,\n    openBelow ? spaceBelow : spaceAbove\n  );\n\n  if (openBelow) {\n    return {\n      left,\n      maxHeight,\n      side: \"bottom\",\n      top: triggerRect.bottom + DROPDOWN_SIDE_OFFSET,\n    };\n  }\n\n  return {\n    left,\n    maxHeight,\n    side: \"top\",\n    bottom: viewportHeight - triggerRect.top + DROPDOWN_SIDE_OFFSET,\n  };\n}\n\nfunction DropdownMenuDivider() {\n  return <hr className=\"mx-2 h-px border-0 bg-border\" />;\n}\n\nfunction DropdownOption({\n  label,\n  selected,\n  onSelect,\n  activeItemId,\n  highlightLayoutId,\n  setActiveItemId,\n}: {\n  label: string;\n  selected: boolean;\n  onSelect: () => void;\n  activeItemId: string | null;\n  highlightLayoutId: string;\n  setActiveItemId: (itemId: string | null) => void;\n}) {\n  const itemId = useId();\n  const isActive = activeItemId === itemId;\n\n  return (\n    <button\n      aria-checked={selected}\n      className={`${dropdownOptionClassName} ${selected ? \"text-foreground\" : \"text-muted-foreground\"}`}\n      onClick={onSelect}\n      onKeyDown={(event) => activateOnEnterOrSpace(event, onSelect)}\n      onMouseDown={preventFocusSteal}\n      onMouseEnter={() => setActiveItemId(itemId)}\n      onPointerMove={() => setActiveItemId(itemId)}\n      role=\"menuitemradio\"\n      tabIndex={-1}\n      type=\"button\"\n    >\n      {isActive ? (\n        <motion.span\n          className={dropdownOptionHighlightClassName}\n          layoutId={highlightLayoutId}\n          transition={dropdownHighlightSpring}\n        />\n      ) : null}\n      <motion.span\n        className=\"relative z-10 flex min-w-0 flex-1 items-center gap-2 truncate\"\n        transition={dropdownLabelSpring}\n      >\n        {label}\n      </motion.span>\n      {selected ? (\n        <motion.span\n          animate={{ opacity: 1, scale: 1, y: 0 }}\n          className=\"relative z-10 flex size-5 shrink-0 items-center justify-center text-foreground\"\n          initial={{ opacity: 0, scale: 0.96, y: 1 }}\n          transition={dropdownCheckSpring}\n        >\n          <Check className=\"h-4 w-4\" />\n        </motion.span>\n      ) : null}\n    </button>\n  );\n}\n\nfunction DropdownActionItem({\n  label,\n  icon,\n  shortcut,\n  onSelect,\n  activeItemId,\n  highlightLayoutId,\n  setActiveItemId,\n}: {\n  label: string;\n  icon?: ReactNode;\n  shortcut?: string;\n  onSelect: () => void;\n  activeItemId: string | null;\n  highlightLayoutId: string;\n  setActiveItemId: (itemId: string | null) => void;\n}) {\n  const itemId = useId();\n  const isActive = activeItemId === itemId;\n\n  return (\n    <button\n      className={`${dropdownOptionClassName} text-foreground`}\n      onClick={onSelect}\n      onKeyDown={(event) => activateOnEnterOrSpace(event, onSelect)}\n      onMouseDown={preventFocusSteal}\n      onMouseEnter={() => setActiveItemId(itemId)}\n      onPointerMove={() => setActiveItemId(itemId)}\n      role=\"menuitem\"\n      tabIndex={-1}\n      type=\"button\"\n    >\n      {isActive ? (\n        <motion.span\n          className={dropdownOptionHighlightClassName}\n          layoutId={highlightLayoutId}\n          transition={dropdownHighlightSpring}\n        />\n      ) : null}\n      <motion.span\n        className=\"relative z-10 flex min-w-0 flex-1 items-center gap-2.5 truncate\"\n        transition={dropdownLabelSpring}\n      >\n        {icon ? (\n          <span className=\"flex size-5 shrink-0 items-center justify-center text-muted-foreground\">\n            {icon}\n          </span>\n        ) : null}\n        <span className=\"truncate\">{label}</span>\n      </motion.span>\n      {shortcut ? (\n        <span className=\"relative z-10 shrink-0 text-muted-foreground text-xs tabular-nums\">\n          {shortcut}\n        </span>\n      ) : null}\n    </button>\n  );\n}\n\nfunction useSubmenuPosition({\n  isCompact,\n  isOpen,\n  submenuRef,\n  triggerRef,\n}: {\n  isCompact: boolean;\n  isOpen: boolean;\n  submenuRef: React.RefObject<HTMLDivElement | null>;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n}) {\n  const [submenuPosition, setSubmenuPosition] = useState<SubmenuPosition>({\n    left: 0,\n    maxHeight: DROPDOWN_ESTIMATED_HEIGHT,\n    origin: \"right\",\n    top: 0,\n  });\n\n  const updateSubmenuPosition = useCallback(() => {\n    const trigger = triggerRef.current;\n    const submenu = submenuRef.current;\n    if (!trigger) return;\n\n    const panelHeight = submenu?.offsetHeight ?? DROPDOWN_ESTIMATED_HEIGHT;\n    const panelWidth = submenu?.offsetWidth ?? DROPDOWN_PANEL_WIDTH;\n\n    setSubmenuPosition(\n      getSubmenuPosition(\n        trigger.getBoundingClientRect(),\n        panelHeight,\n        panelWidth\n      )\n    );\n  }, [submenuRef, triggerRef]);\n\n  useLayoutEffect(() => {\n    if (!isOpen || isCompact) return;\n\n    updateSubmenuPosition();\n\n    const submenu = submenuRef.current;\n    let observer: ResizeObserver | undefined;\n\n    if (submenu) {\n      observer = new ResizeObserver(updateSubmenuPosition);\n      observer.observe(submenu);\n    }\n\n    window.addEventListener(\"resize\", updateSubmenuPosition);\n    window.addEventListener(\"scroll\", updateSubmenuPosition, true);\n\n    return () => {\n      observer?.disconnect();\n      window.removeEventListener(\"resize\", updateSubmenuPosition);\n      window.removeEventListener(\"scroll\", updateSubmenuPosition, true);\n    };\n  }, [isCompact, isOpen, submenuRef, updateSubmenuPosition]);\n\n  return { submenuPosition, updateSubmenuPosition };\n}\n\nfunction SubmenuInlinePanel({\n  isOpen,\n  submenuOptions,\n}: {\n  isOpen: boolean;\n  submenuOptions: ReactNode;\n}) {\n  if (!isOpen) return null;\n  return <div className=\"px-1 pb-1\">{submenuOptions}</div>;\n}\n\nfunction SubmenuFlyoutPanel({\n  flyoutSubmenuMotion,\n  groupLabel,\n  isOpen,\n  itemId,\n  onOpenChange,\n  prefersHover,\n  setActiveItemId,\n  submenuHighlightLayoutId,\n  submenuId,\n  submenuOptions,\n  submenuPosition,\n  submenuRef,\n  triggerRef,\n}: {\n  flyoutSubmenuMotion: ReturnType<typeof getFlyoutSubmenuMotion>;\n  groupLabel: string;\n  isOpen: boolean;\n  itemId: string;\n  onOpenChange: (open: boolean) => void;\n  prefersHover: boolean;\n  setActiveItemId: (itemId: string | null) => void;\n  submenuHighlightLayoutId: string;\n  submenuId: string;\n  submenuOptions: ReactNode;\n  submenuPosition: SubmenuPosition;\n  submenuRef: React.RefObject<HTMLDivElement | null>;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n}) {\n  return (\n    <AnimatePresence initial={false}>\n      {isOpen ? (\n        <motion.div\n          animate={flyoutSubmenuMotion.animate}\n          aria-label={groupLabel}\n          className={dropdownPanelClassName}\n          data-prompt-dropdown-panel=\"\"\n          exit={flyoutSubmenuMotion.exit}\n          initial={flyoutSubmenuMotion.initial}\n          key={submenuId}\n          onMouseEnter={() => setActiveItemId(itemId)}\n          onMouseLeave={\n            prefersHover\n              ? (event) => {\n                  const related = event.relatedTarget as Node | null;\n                  if (triggerRef.current?.contains(related)) return;\n                  if (isInsideDropdownPanel(related)) return;\n                  onOpenChange(false);\n                }\n              : undefined\n          }\n          ref={submenuRef}\n          role=\"menu\"\n          style={{\n            left: submenuPosition.left,\n            maxHeight: submenuPosition.maxHeight,\n            top: submenuPosition.top,\n            transformOrigin:\n              submenuPosition.origin === \"right\" ? \"left top\" : \"right top\",\n            zIndex: 401,\n          }}\n          transition={flyoutSubmenuMotion.transition}\n        >\n          <LayoutGroup id={`${submenuHighlightLayoutId}-group`}>\n            <motion.div\n              animate={{ opacity: 1, y: 0 }}\n              exit={{\n                opacity: 0,\n                y: flyoutSubmenuMotion.initial.x > 0 ? 3 : -3,\n              }}\n              initial={{\n                opacity: 0,\n                y: flyoutSubmenuMotion.initial.x > 0 ? -3 : 3,\n              }}\n              transition={{ duration: 0.18, ease: [0.22, 1, 0.36, 1] }}\n            >\n              {submenuOptions}\n            </motion.div>\n          </LayoutGroup>\n        </motion.div>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nfunction DropdownSubmenu({\n  group,\n  selectedValue,\n  triggerLabel,\n  valueLabel,\n  icon,\n  submenuId,\n  isOpen,\n  onOpenChange,\n  onSelect,\n  activeItemId,\n  highlightLayoutId,\n  submenuHighlightLayoutId,\n  setActiveItemId,\n}: {\n  group: PromptSettingGroup;\n  selectedValue: string;\n  triggerLabel: string;\n  valueLabel?: string;\n  icon?: ReactNode;\n  submenuId: string;\n  isOpen: boolean;\n  onOpenChange: (open: boolean) => void;\n  onSelect: (value: string) => void;\n  activeItemId: string | null;\n  highlightLayoutId: string;\n  submenuHighlightLayoutId: string;\n  setActiveItemId: (itemId: string | null) => void;\n}) {\n  const itemId = useId();\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const submenuRef = useRef<HTMLDivElement>(null);\n  const isCompact = useCompactViewport();\n  const prefersHover = usePrefersHover();\n  const mounted = useIsMounted();\n  const { submenuPosition, updateSubmenuPosition } = useSubmenuPosition({\n    isCompact,\n    isOpen,\n    submenuRef,\n    triggerRef,\n  });\n  const isActive = activeItemId === itemId || isOpen;\n  const flyoutSubmenuMotion = getFlyoutSubmenuMotion(submenuPosition.origin);\n  const wasCompactRef = useRef<boolean | null>(null);\n\n  useEffect(() => {\n    const previousCompact = wasCompactRef.current;\n    wasCompactRef.current = isCompact;\n    if (previousCompact === false && isCompact && isOpen) {\n      onOpenChange(false);\n    }\n  }, [isCompact, isOpen, onOpenChange]);\n\n  const openSubmenu = useCallback(() => {\n    if (isCompact) return;\n    updateSubmenuPosition();\n    onOpenChange(true);\n  }, [isCompact, onOpenChange, updateSubmenuPosition]);\n\n  const toggleSubmenu = useCallback(() => {\n    setActiveItemId(itemId);\n    onOpenChange(!isOpen);\n  }, [isOpen, itemId, onOpenChange, setActiveItemId]);\n\n  const handleTriggerMouseLeave = useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      const related = event.relatedTarget as Node | null;\n      if (submenuRef.current?.contains(related)) return;\n      if (isInsideDropdownPanel(related)) return;\n      if (isOpen) onOpenChange(false);\n    },\n    [isOpen, onOpenChange]\n  );\n\n  const handleOptionSelect = useCallback(\n    (value: string) => {\n      flushSync(() => {\n        onOpenChange(false);\n      });\n      onSelect(value);\n    },\n    [onOpenChange, onSelect]\n  );\n\n  const submenuOptions = (\n    <div className=\"space-y-0.5\">\n      {group.options.map((option) => (\n        <DropdownOption\n          activeItemId={activeItemId}\n          highlightLayoutId={submenuHighlightLayoutId}\n          key={option.value}\n          label={option.label}\n          onSelect={() => handleOptionSelect(option.value)}\n          selected={selectedValue === option.value}\n          setActiveItemId={setActiveItemId}\n        />\n      ))}\n    </div>\n  );\n\n  return (\n    <>\n      <button\n        aria-expanded={isOpen}\n        aria-haspopup=\"menu\"\n        className={`${dropdownSubmenuTriggerClassName} touch-manipulation text-foreground`}\n        onClick={toggleSubmenu}\n        onKeyDown={(event) => activateOnEnterOrSpace(event, toggleSubmenu)}\n        onMouseDown={preventFocusSteal}\n        onMouseEnter={\n          prefersHover\n            ? () => {\n                setActiveItemId(itemId);\n                openSubmenu();\n              }\n            : undefined\n        }\n        onMouseLeave={prefersHover ? handleTriggerMouseLeave : undefined}\n        onPointerMove={prefersHover ? () => setActiveItemId(itemId) : undefined}\n        ref={triggerRef}\n        tabIndex={-1}\n        type=\"button\"\n      >\n        {isActive ? (\n          <motion.span\n            className={dropdownOptionHighlightClassName}\n            layoutId={highlightLayoutId}\n            transition={dropdownHighlightSpring}\n          />\n        ) : null}\n        <motion.span\n          className=\"relative z-10 flex min-w-0 flex-1 items-center gap-2.5 truncate\"\n          transition={dropdownLabelSpring}\n        >\n          {icon ? (\n            <span className=\"flex size-5 shrink-0 items-center justify-center text-muted-foreground\">\n              {icon}\n            </span>\n          ) : null}\n          <span className=\"truncate\">{triggerLabel}</span>\n        </motion.span>\n        <span className=\"relative z-10 flex shrink-0 items-center gap-1.5\">\n          {valueLabel ? (\n            <span className=\"max-w-[5.5rem] truncate text-muted-foreground sm:max-w-none\">\n              {valueLabel}\n            </span>\n          ) : null}\n          <ChevronRight\n            aria-hidden=\"true\"\n            className={`size-4 text-muted-foreground transition-transform ${isCompact && isOpen ? \"rotate-90\" : \"\"}`}\n          />\n        </span>\n      </button>\n      {isCompact ? (\n        <SubmenuInlinePanel isOpen={isOpen} submenuOptions={submenuOptions} />\n      ) : null}\n      {!isCompact && mounted\n        ? createPortal(\n            <SubmenuFlyoutPanel\n              flyoutSubmenuMotion={flyoutSubmenuMotion}\n              groupLabel={group.label}\n              isOpen={isOpen}\n              itemId={itemId}\n              onOpenChange={onOpenChange}\n              prefersHover={prefersHover}\n              setActiveItemId={setActiveItemId}\n              submenuHighlightLayoutId={submenuHighlightLayoutId}\n              submenuId={submenuId}\n              submenuOptions={submenuOptions}\n              submenuPosition={submenuPosition}\n              submenuRef={submenuRef}\n              triggerRef={triggerRef}\n            />,\n            document.body\n          )\n        : null}\n    </>\n  );\n}\n\nexport function SettingsDropdown({\n  groups,\n  values,\n  menuActions = [],\n  onOpenChange,\n  onValueChange,\n}: {\n  groups: PromptSettingGroup[];\n  values: Record<string, string>;\n  menuActions?: PromptMenuAction[];\n  onOpenChange?: (open: boolean) => void;\n  onValueChange: (groupId: string, value: string) => void;\n}) {\n  const [open, setOpen] = useState(false);\n  const [openSubmenuId, setOpenSubmenuId] = useState<string | null>(null);\n  const [activeItemId, setActiveItemId] = useState<string | null>(null);\n  const highlightLayoutId = useId();\n  const [position, setPosition] = useState<DropdownPosition>({\n    left: 0,\n    maxHeight: DROPDOWN_ESTIMATED_HEIGHT,\n    side: \"bottom\",\n    top: 0,\n  });\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const prefersHover = usePrefersHover();\n  const isCompact = useCompactViewport();\n  const mounted = useIsMounted();\n  const [instantDismiss, setInstantDismiss] = useState(false);\n\n  const handleOpenChange = useCallback(\n    (nextOpen: boolean) => {\n      setOpen(nextOpen);\n      onOpenChange?.(nextOpen);\n      if (!nextOpen) {\n        setActiveItemId(null);\n        setOpenSubmenuId(null);\n      }\n    },\n    [onOpenChange]\n  );\n\n  const dismissDropdown = useCallback(\n    (instant = false) => {\n      if (instant) setInstantDismiss(true);\n      handleOpenChange(false);\n    },\n    [handleOpenChange]\n  );\n\n  useEffect(() => {\n    if (open) setInstantDismiss(false);\n  }, [open]);\n\n  const handleSubmenuSelect = useCallback(\n    (groupId: string, value: string) => {\n      onValueChange(groupId, value);\n      setOpenSubmenuId(null);\n      if (!isCompact) dismissDropdown(true);\n    },\n    [dismissDropdown, isCompact, onValueChange]\n  );\n\n  const updatePosition = useCallback(() => {\n    const trigger = triggerRef.current;\n    if (!trigger) return;\n\n    const triggerRect = trigger.getBoundingClientRect();\n    const panel = panelRef.current;\n    const panelHeight = panel?.offsetHeight ?? DROPDOWN_ESTIMATED_HEIGHT;\n    const panelWidth = panel?.offsetWidth ?? DROPDOWN_PANEL_WIDTH;\n\n    setPosition(getDropdownPosition({ panelHeight, panelWidth, triggerRect }));\n  }, []);\n\n  useLayoutEffect(() => {\n    if (!open) return;\n\n    updatePosition();\n\n    const panel = panelRef.current;\n    let observer: ResizeObserver | undefined;\n\n    if (panel) {\n      observer = new ResizeObserver(() => updatePosition());\n      observer.observe(panel);\n    }\n\n    window.addEventListener(\"resize\", updatePosition);\n    window.addEventListener(\"scroll\", updatePosition, true);\n\n    return () => {\n      observer?.disconnect();\n      window.removeEventListener(\"resize\", updatePosition);\n      window.removeEventListener(\"scroll\", updatePosition, true);\n    };\n  }, [open, updatePosition]);\n\n  const handleEscape = useCallback(() => {\n    if (openSubmenuId) {\n      setOpenSubmenuId(null);\n      return true;\n    }\n    return false;\n  }, [openSubmenuId]);\n\n  useDropdownDismiss({\n    onClose: () => handleOpenChange(false),\n    onEscape: handleEscape,\n    open,\n    triggerRef,\n  });\n\n  const toggleOpen = useCallback(() => {\n    if (open) {\n      handleOpenChange(false);\n      return;\n    }\n    updatePosition();\n    handleOpenChange(true);\n  }, [handleOpenChange, open, updatePosition]);\n\n  const selectedLabels = groups.map((group) =>\n    getOptionLabel(group, values[group.id] ?? \"\")\n  );\n  const triggerLabel = selectedLabels.join(\", \");\n  const effortGroupId = getEffortGroupId(groups);\n  const effortLevel = normalizeEffortLevel(\n    effortGroupId ? (values[effortGroupId] ?? \"medium\") : \"medium\"\n  );\n  const mainDropdownMotion = getMainDropdownMotion(\n    position.side,\n    instantDismiss\n  );\n  const mainDropdownInnerMotion = getMainDropdownInnerMotion(\n    position.side,\n    instantDismiss\n  );\n\n  return (\n    <>\n      <button\n        aria-expanded={open}\n        aria-haspopup=\"menu\"\n        aria-label={`Select settings: ${triggerLabel}`}\n        className=\"flex min-w-0 max-w-[calc(100%-3rem)] items-center gap-1.5 rounded-full py-1 text-sm transition-colors hover:text-foreground\"\n        onClick={toggleOpen}\n        onMouseDown={(event) => event.preventDefault()}\n        ref={triggerRef}\n        type=\"button\"\n      >\n        <span className=\"shrink-0 text-muted-foreground\">\n          <EffortBarsIcon level={effortLevel} />\n        </span>\n        {groups.map((group, index) => {\n          const label = getOptionLabel(group, values[group.id] ?? \"\");\n\n          return (\n            <span\n              className={`truncate font-medium ${index === 0 ? \"text-foreground\" : \"text-muted-foreground\"}`}\n              key={group.id}\n            >\n              {label}\n            </span>\n          );\n        })}\n        <span\n          className={`text-muted-foreground transition-transform ${open ? \"rotate-180 text-foreground\" : \"\"}`}\n        >\n          <ChevronDownIcon />\n        </span>\n      </button>\n      {mounted &&\n        createPortal(\n          <AnimatePresence initial={false}>\n            {open ? (\n              <motion.div\n                animate={mainDropdownMotion.animate}\n                aria-label=\"Prompt settings\"\n                className={dropdownPanelClassName}\n                data-prompt-dropdown-panel=\"\"\n                exit={mainDropdownMotion.exit}\n                initial={mainDropdownMotion.initial}\n                key=\"prompt-settings-dropdown\"\n                ref={panelRef}\n                role=\"menu\"\n                style={{\n                  left: position.left,\n                  maxHeight: position.maxHeight,\n                  transformOrigin:\n                    position.side === \"bottom\" ? \"top left\" : \"bottom left\",\n                  ...(position.side === \"bottom\"\n                    ? { top: position.top }\n                    : { bottom: position.bottom }),\n                }}\n                transition={mainDropdownMotion.transition}\n              >\n                <LayoutGroup>\n                  <motion.div\n                    animate={mainDropdownInnerMotion.animate}\n                    className=\"space-y-0.5\"\n                    exit={mainDropdownInnerMotion.exit}\n                    initial={mainDropdownInnerMotion.initial}\n                    onMouseLeave={\n                      prefersHover\n                        ? (event) => {\n                            const related = event.relatedTarget as Node | null;\n                            if (\n                              related instanceof Element &&\n                              related.closest(\"[data-prompt-dropdown-panel]\")\n                            ) {\n                              return;\n                            }\n                            setActiveItemId(null);\n                            setOpenSubmenuId(null);\n                          }\n                        : undefined\n                    }\n                    transition={mainDropdownInnerMotion.transition}\n                  >\n                    {menuActions.map((action, index) => (\n                      <DropdownActionItem\n                        activeItemId={activeItemId}\n                        highlightLayoutId={`${highlightLayoutId}-action-${index}`}\n                        icon={action.icon}\n                        key={`${action.label}-${index}`}\n                        label={action.label}\n                        onSelect={() => {\n                          action.onSelect();\n                          handleOpenChange(false);\n                        }}\n                        setActiveItemId={setActiveItemId}\n                      />\n                    ))}\n                    {menuActions.length > 0 && groups.length > 0 ? (\n                      <DropdownMenuDivider />\n                    ) : null}\n                    {groups\n                      .filter((group) => group.display === \"featured\")\n                      .map((group) => {\n                        const selected = getSelectedOption(\n                          group,\n                          values[group.id] ?? \"\"\n                        );\n                        if (!selected) return null;\n\n                        return (\n                          <DropdownFeaturedRow\n                            description={selected.description}\n                            key={`${group.id}-featured`}\n                            label={selected.label}\n                          />\n                        );\n                      })}\n                    {groups\n                      .filter((group) => group.display === \"submenu\")\n                      .map((group) => (\n                        <Fragment key={group.id}>\n                          <DropdownMenuDivider />\n                          <DropdownSubmenu\n                            activeItemId={activeItemId}\n                            group={group}\n                            highlightLayoutId={highlightLayoutId}\n                            isOpen={openSubmenuId === group.id}\n                            onOpenChange={(nextOpen) => {\n                              setOpenSubmenuId(nextOpen ? group.id : null);\n                            }}\n                            onSelect={(value) => {\n                              handleSubmenuSelect(group.id, value);\n                            }}\n                            selectedValue={values[group.id] ?? \"\"}\n                            setActiveItemId={setActiveItemId}\n                            submenuHighlightLayoutId={`${highlightLayoutId}-${group.id}-sub`}\n                            submenuId={group.id}\n                            triggerLabel={group.label}\n                            valueLabel={getOptionLabel(\n                              group,\n                              values[group.id] ?? \"\"\n                            )}\n                          />\n                        </Fragment>\n                      ))}\n                    {groups\n                      .filter((group) => group.moreMenuLabel)\n                      .map((group) => (\n                        <Fragment key={`${group.id}-more`}>\n                          <DropdownMenuDivider />\n                          <DropdownSubmenu\n                            activeItemId={activeItemId}\n                            group={group}\n                            highlightLayoutId={highlightLayoutId}\n                            isOpen={openSubmenuId === `${group.id}-more`}\n                            onOpenChange={(nextOpen) => {\n                              setOpenSubmenuId(\n                                nextOpen ? `${group.id}-more` : null\n                              );\n                            }}\n                            onSelect={(value) => {\n                              handleSubmenuSelect(group.id, value);\n                            }}\n                            selectedValue={values[group.id] ?? \"\"}\n                            setActiveItemId={setActiveItemId}\n                            submenuHighlightLayoutId={`${highlightLayoutId}-${group.id}-more-sub`}\n                            submenuId={`${group.id}-more`}\n                            triggerLabel={group.moreMenuLabel ?? group.label}\n                          />\n                        </Fragment>\n                      ))}\n                  </motion.div>\n                </LayoutGroup>\n              </motion.div>\n            ) : null}\n          </AnimatePresence>,\n          document.body\n        )}\n    </>\n  );\n}\n\n// ─── Plus menu ───────────────────────────────────────────────────────────────\n\nexport type AIInputMenuItem = {\n  value: string;\n  /** Not needed for separators. */\n  label?: string;\n  icon?: ComponentType<{ className?: string }>;\n  /** \"action\" (default) fires onMenuSelect, \"toggle\" renders a switch, \"separator\" draws a divider. */\n  type?: \"action\" | \"toggle\" | \"separator\";\n  /** Initial state for toggle items. */\n  defaultChecked?: boolean;\n  /** Keyboard shortcut shown on the right of action items (e.g. \"⌘U\"). */\n  shortcut?: string;\n  /** Called when this action item is clicked, in addition to onMenuSelect. */\n  onClick?: () => void;\n  /** Called when this toggle item changes, in addition to onMenuToggle. */\n  onCheckedChange?: (checked: boolean) => void;\n  /** Nested items turn an action into a submenu (one level deep). */\n  items?: AIInputMenuItem[];\n};\n\nfunction OptionMenu({\n  align = \"start\",\n  ariaLabel,\n  chipClassName,\n  onChange,\n  options,\n  value,\n}: {\n  align?: \"start\" | \"end\";\n  ariaLabel: string;\n  chipClassName: string;\n  onChange: (value: string) => void;\n  options: AIInputOption[];\n  value: string;\n}) {\n  const [open, setOpen] = useState(false);\n  const [upward, setUpward] = useState(false);\n  const [hoveredValue, setHoveredValue] = useState<string | null>(null);\n  const hoverLayoutId = useId();\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!open) return;\n\n    const handlePointerDown = (event: PointerEvent) => {\n      if (containerRef.current?.contains(event.target as Node)) return;\n      setOpen(false);\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") setOpen(false);\n    };\n\n    document.addEventListener(\"pointerdown\", handlePointerDown);\n    document.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      document.removeEventListener(\"pointerdown\", handlePointerDown);\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [open]);\n\n  const selected =\n    options.find((option) => option.value === value) ?? options[0];\n\n  return (\n    <div className=\"relative min-w-0\" ref={containerRef}>\n      <button\n        aria-expanded={open}\n        aria-haspopup=\"menu\"\n        aria-label={ariaLabel}\n        className={chipClassName}\n        onClick={() => {\n          if (!open && containerRef.current) {\n            const rect = containerRef.current.getBoundingClientRect();\n            setUpward(window.innerHeight - rect.bottom < 160);\n          }\n          setHoveredValue(null);\n          setOpen((previous) => !previous);\n        }}\n        type=\"button\"\n      >\n        <span className=\"truncate\">{selected?.label}</span>\n        <ChevronDown\n          aria-hidden=\"true\"\n          className={`size-3.5 shrink-0 text-muted-foreground transition-transform ${open ? \"rotate-180\" : \"\"}`}\n        />\n      </button>\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            aria-label={ariaLabel}\n            className={`absolute z-50 w-max min-w-52 max-w-[calc(100vw-2rem)] rounded-2xl border border-border bg-card p-1 shadow-[0_10px_28px_-18px_rgba(0,0,0,0.25)] ${align === \"end\" ? \"right-0\" : \"left-0\"} ${upward ? `bottom-full mb-2 ${align === \"end\" ? \"origin-bottom-right\" : \"origin-bottom-left\"}` : `top-full mt-2 ${align === \"end\" ? \"origin-top-right\" : \"origin-top-left\"}`}`}\n            exit={{ opacity: 0, scale: 0.97, y: upward ? 4 : -4 }}\n            initial={{ opacity: 0, scale: 0.96, y: upward ? 6 : -6 }}\n            onMouseLeave={() => setHoveredValue(null)}\n            role=\"menu\"\n            transition={{ duration: 0.16, ease: \"easeOut\" }}\n          >\n            {options.map((option) => (\n              <button\n                aria-checked={option.value === selected?.value}\n                className=\"relative isolate flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-left text-sm\"\n                key={option.value}\n                onClick={() => {\n                  onChange(option.value);\n                  setOpen(false);\n                }}\n                onMouseEnter={() => setHoveredValue(option.value)}\n                role=\"menuitemradio\"\n                type=\"button\"\n              >\n                {hoveredValue === option.value ? (\n                  <motion.span\n                    className=\"absolute inset-0 -z-10 rounded-[inherit] bg-accent/70\"\n                    layoutId={hoverLayoutId}\n                    transition={HOVER_SPRING}\n                  />\n                ) : null}\n                <span className=\"truncate\">{option.label}</span>\n                {option.value === selected?.value ? (\n                  <Check\n                    aria-hidden=\"true\"\n                    className=\"size-4 shrink-0 text-foreground\"\n                  />\n                ) : null}\n              </button>\n            ))}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction SendWave({ reducedMotion }: { reducedMotion: boolean }) {\n  return (\n    <motion.div\n      animate={{ opacity: 1 }}\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute inset-0 z-10 overflow-hidden rounded-[inherit]\"\n      exit={{ opacity: 0 }}\n      initial={{ opacity: 1 }}\n      transition={{ duration: 0.25, ease: \"easeOut\" }}\n    >\n      <motion.div\n        animate={reducedMotion ? { y: \"-17%\" } : { y: \"-105%\" }}\n        className=\"absolute inset-x-0 top-0 h-[150%] blur-xl\"\n        initial={{ y: \"55%\" }}\n        style={{ background: WAVE_WASH_GRADIENT }}\n        transition={\n          reducedMotion ? { duration: 0 } : { duration: 0.7, ease: \"easeOut\" }\n        }\n      />\n    </motion.div>\n  );\n}\n\nconst PLUS_PANEL_HEIGHT = 260;\n\nconst plusSlideVariants = {\n  enter: (dir: number) => ({ opacity: 0, x: dir * 10 }),\n  visible: { opacity: 1, x: 0 },\n  exit: (dir: number) => ({ opacity: 0, x: dir * -10 }),\n};\n\nfunction collectToggleDefaults(items: AIInputMenuItem[]) {\n  const defaults: Record<string, boolean> = {};\n\n  for (const item of items) {\n    if (item.type === \"toggle\") {\n      defaults[item.value] = item.defaultChecked ?? false;\n    }\n    for (const child of item.items ?? []) {\n      if (child.type === \"toggle\") {\n        defaults[child.value] = child.defaultChecked ?? false;\n      }\n    }\n  }\n\n  return defaults;\n}\n\nfunction PlusMenuIcon({\n  icon: Icon,\n}: {\n  icon?: ComponentType<{ className?: string }>;\n}) {\n  if (!Icon) return null;\n  return (\n    <span aria-hidden=\"true\">\n      <Icon className=\"size-[15px] shrink-0 text-foreground/60\" />\n    </span>\n  );\n}\n\nfunction PlusMenu({\n  items,\n  onSelect,\n  onToggle,\n}: {\n  items: AIInputMenuItem[];\n  onSelect?: (value: string) => void;\n  onToggle?: (value: string, checked: boolean) => void;\n}) {\n  const [open, setOpen] = useState(false);\n  const [upward, setUpward] = useState(false);\n  const [toggles, setToggles] = useState<Record<string, boolean>>(() =>\n    collectToggleDefaults(items)\n  );\n  const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);\n  const [slideDir, setSlideDir] = useState(1);\n  const [hoveredValue, setHoveredValue] = useState<string | null>(null);\n  const hoverLayoutId = useId();\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!open) return;\n\n    const handlePointerDown = (event: PointerEvent) => {\n      if (containerRef.current?.contains(event.target as Node)) return;\n      setOpen(false);\n    };\n\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") setOpen(false);\n    };\n\n    document.addEventListener(\"pointerdown\", handlePointerDown);\n    document.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      document.removeEventListener(\"pointerdown\", handlePointerDown);\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [open]);\n\n  const handleOpen = () => {\n    if (!open) {\n      if (containerRef.current) {\n        const rect = containerRef.current.getBoundingClientRect();\n        setUpward(window.innerHeight - rect.bottom < PLUS_PANEL_HEIGHT);\n      }\n      setActiveSubmenu(null);\n      setSlideDir(1);\n      setHoveredValue(null);\n    }\n    setOpen((previous) => !previous);\n  };\n\n  const handleToggle = (item: AIInputMenuItem, checked: boolean) => {\n    setToggles((previous) => ({ ...previous, [item.value]: checked }));\n    item.onCheckedChange?.(checked);\n    onToggle?.(item.value, checked);\n  };\n\n  const handleAction = (item: AIInputMenuItem) => {\n    item.onClick?.();\n    onSelect?.(item.value);\n    setOpen(false);\n  };\n\n  const openSub = (value: string) => {\n    setSlideDir(1);\n    setActiveSubmenu(value);\n    setHoveredValue(null);\n  };\n\n  const closeSub = () => {\n    setSlideDir(-1);\n    setActiveSubmenu(null);\n    setHoveredValue(null);\n  };\n\n  const hoverHighlight = (key: string) =>\n    hoveredValue === key ? (\n      <motion.span\n        className=\"absolute inset-0 -z-10 rounded-[inherit] bg-accent/70\"\n        layoutId={hoverLayoutId}\n        transition={HOVER_SPRING}\n      />\n    ) : null;\n\n  const renderItem = (item: AIInputMenuItem, inSubmenu: boolean) => {\n    if (item.type === \"separator\") {\n      return <div className=\"mx-1 my-0.5 h-px bg-border\" key={item.value} />;\n    }\n\n    if (item.type === \"toggle\") {\n      return (\n        <div\n          className=\"relative isolate flex items-center justify-between gap-3 rounded-xl px-3 py-1.5\"\n          key={item.value}\n          onMouseEnter={() => setHoveredValue(item.value)}\n        >\n          {hoverHighlight(item.value)}\n          <div className=\"flex items-center gap-3\">\n            <PlusMenuIcon icon={item.icon} />\n            <span className=\"text-sm\">{item.label}</span>\n          </div>\n          <Switch\n            checked={toggles[item.value] ?? false}\n            onCheckedChange={(checked) => handleToggle(item, checked)}\n            size=\"sm\"\n          />\n        </div>\n      );\n    }\n\n    if (!inSubmenu && item.items && item.items.length > 0) {\n      return (\n        <button\n          className=\"relative isolate flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-left text-sm\"\n          key={item.value}\n          onClick={() => openSub(item.value)}\n          onMouseEnter={() => setHoveredValue(item.value)}\n          type=\"button\"\n        >\n          {hoverHighlight(item.value)}\n          <div className=\"flex items-center gap-3\">\n            <PlusMenuIcon icon={item.icon} />\n            {item.label}\n          </div>\n          <ChevronRight\n            aria-hidden=\"true\"\n            className=\"size-3.5 text-foreground/40\"\n          />\n        </button>\n      );\n    }\n\n    return (\n      <button\n        className=\"relative isolate flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2 text-left text-sm\"\n        key={item.value}\n        onClick={() => handleAction(item)}\n        onMouseEnter={() => setHoveredValue(item.value)}\n        type=\"button\"\n      >\n        {hoverHighlight(item.value)}\n        <div className=\"flex items-center gap-3\">\n          <PlusMenuIcon icon={item.icon} />\n          {item.label}\n        </div>\n        {item.shortcut ? (\n          <span className=\"shrink-0 text-muted-foreground text-xs\">\n            {item.shortcut}\n          </span>\n        ) : null}\n      </button>\n    );\n  };\n\n  const activeParent = activeSubmenu\n    ? items.find((item) => item.value === activeSubmenu)\n    : undefined;\n\n  return (\n    <div className=\"relative\" ref={containerRef}>\n      <button\n        aria-expanded={open}\n        aria-haspopup=\"menu\"\n        aria-label=\"More options\"\n        className=\"flex size-8 shrink-0 items-center justify-center rounded-full text-foreground/70 transition-colors hover:text-foreground sm:size-9\"\n        onClick={handleOpen}\n        type=\"button\"\n      >\n        <Plus aria-hidden=\"true\" className=\"size-4\" />\n      </button>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            aria-label=\"More options\"\n            className={`absolute left-0 z-50 w-max min-w-64 max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border border-border bg-card p-1 shadow-[0_10px_28px_-18px_rgba(0,0,0,0.25)] ${upward ? \"bottom-full mb-2 origin-bottom-left\" : \"top-full mt-2 origin-top-left\"}`}\n            exit={{ opacity: 0, scale: 0.97, y: upward ? 4 : -4 }}\n            initial={{ opacity: 0, scale: 0.96, y: upward ? 6 : -6 }}\n            onMouseLeave={() => setHoveredValue(null)}\n            role=\"menu\"\n            transition={{ duration: 0.16, ease: \"easeOut\" }}\n          >\n            <AnimatePresence custom={slideDir} initial={false} mode=\"wait\">\n              {activeParent ? (\n                <motion.div\n                  animate=\"visible\"\n                  custom={slideDir}\n                  exit=\"exit\"\n                  initial=\"enter\"\n                  key={activeParent.value}\n                  transition={{ duration: 0.13, ease: \"easeOut\" }}\n                  variants={plusSlideVariants}\n                >\n                  <button\n                    className=\"relative isolate flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left font-medium text-sm\"\n                    onClick={closeSub}\n                    onMouseEnter={() => setHoveredValue(\"__back\")}\n                    type=\"button\"\n                  >\n                    {hoverHighlight(\"__back\")}\n                    <ChevronLeft\n                      aria-hidden=\"true\"\n                      className=\"size-4 shrink-0 text-muted-foreground\"\n                    />\n                    {activeParent.label}\n                  </button>\n                  <div className=\"mx-1 my-0.5 h-px bg-border\" />\n                  {(activeParent.items ?? []).map((item) =>\n                    renderItem(item, true)\n                  )}\n                </motion.div>\n              ) : (\n                <motion.div\n                  animate=\"visible\"\n                  custom={slideDir}\n                  exit=\"exit\"\n                  initial=\"enter\"\n                  key=\"main\"\n                  transition={{ duration: 0.13, ease: \"easeOut\" }}\n                  variants={plusSlideVariants}\n                >\n                  {items.map((item) => renderItem(item, false))}\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\n// ─── Public component ────────────────────────────────────────────────────────\n\nexport type AIInputProps = {\n  /** Options for the agent chip on the left. */\n  agents?: AIInputOption[];\n  defaultAgent?: string;\n  /** Setting groups for the settings dropdown (model, effort, etc.). */\n  settingGroups?: PromptSettingGroup[];\n  defaultSettings?: Record<string, string>;\n  /** Action items shown at the top of the settings dropdown. */\n  menuActions?: PromptMenuAction[];\n  /** Items for the plus menu: actions, toggles, separators, and one-level submenus. */\n  menuItems?: AIInputMenuItem[];\n  placeholder?: string;\n  /** Render sent messages as chat bubbles above the composer. */\n  showMessages?: boolean;\n  onSend?: (\n    message: string,\n    meta: { agent: string; settings: Record<string, string> }\n  ) => void;\n  onMicClick?: () => void;\n  /** Fired when the plus button is clicked and no menuItems are provided. */\n  onPlusClick?: () => void;\n  /** Fired when an action item (or submenu item) is selected in the plus menu. */\n  onMenuSelect?: (value: string) => void;\n  /** Fired when a toggle item in the plus menu changes. */\n  onMenuToggle?: (value: string, checked: boolean) => void;\n  /** Fired when the agent chip selection changes. */\n  onAgentChange?: (value: string) => void;\n  /** Fired when any setting in the settings dropdown changes. */\n  onSettingsChange?: (settings: Record<string, string>) => void;\n  className?: string;\n};\n\nexport function AIInput({\n  agents = [],\n  defaultAgent,\n  settingGroups = [],\n  defaultSettings,\n  menuActions = [],\n  menuItems = [],\n  placeholder = \"Ask for follow-up changes\",\n  showMessages = true,\n  onSend,\n  onMicClick,\n  onPlusClick,\n  onMenuSelect,\n  onMenuToggle,\n  onAgentChange,\n  onSettingsChange,\n  className,\n}: AIInputProps) {\n  const [value, setValue] = useState(\"\");\n  const [messages, setMessages] = useState<AIInputMessage[]>([]);\n  const [waveRun, setWaveRun] = useState(0);\n  const [agent, setAgent] = useState(\n    () => defaultAgent ?? agents[0]?.value ?? \"\"\n  );\n  const [internalSettings, setInternalSettings] = useState(() =>\n    getDefaultSettings(settingGroups, defaultSettings)\n  );\n  const nextMessageIdRef = useRef(0);\n  const waveTimeoutRef = useRef<number | undefined>(undefined);\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const messagesRef = useRef<HTMLDivElement>(null);\n  const reducedMotion = useReducedMotion() ?? false;\n  const hasValue = value.trim() !== \"\";\n\n  useEffect(() => () => window.clearTimeout(waveTimeoutRef.current), []);\n\n  const syncHeight = useCallback(() => {\n    const element = textareaRef.current;\n    if (!element) return;\n\n    element.style.height = \"auto\";\n    element.style.height = `${Math.min(element.scrollHeight, MAX_TEXTAREA_HEIGHT)}px`;\n  }, []);\n\n  const handleSend = useCallback(() => {\n    const trimmed = value.trim();\n    if (trimmed === \"\") return;\n\n    nextMessageIdRef.current += 1;\n    setMessages((previous) => [\n      ...previous,\n      { id: nextMessageIdRef.current, text: trimmed },\n    ]);\n    onSend?.(trimmed, { agent, settings: internalSettings });\n    setValue(\"\");\n\n    requestAnimationFrame(() => {\n      const element = textareaRef.current;\n      if (element) element.style.height = \"auto\";\n\n      messagesRef.current?.scrollTo({\n        top: messagesRef.current.scrollHeight,\n        behavior: reducedMotion ? \"auto\" : \"smooth\",\n      });\n    });\n\n    setWaveRun((run) => run + 1);\n    window.clearTimeout(waveTimeoutRef.current);\n    waveTimeoutRef.current = window.setTimeout(() => {\n      setWaveRun(0);\n    }, WAVE_DURATION_MS);\n  }, [agent, internalSettings, onSend, reducedMotion, value]);\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n      if (event.key === \"Enter\" && !event.shiftKey) {\n        event.preventDefault();\n        handleSend();\n      }\n    },\n    [handleSend]\n  );\n\n  return (\n    <div className={`w-full ${className ?? \"\"}`}>\n      {showMessages && messages.length > 0 ? (\n        <div\n          className=\"mb-4 flex max-h-72 flex-col gap-2 overflow-y-auto overscroll-contain px-1 py-1\"\n          ref={messagesRef}\n        >\n          <AnimatePresence initial={false}>\n            {messages.map((message) => (\n              <motion.div\n                animate={{ opacity: 1, scale: 1, y: 0 }}\n                className=\"max-w-[85%] self-end whitespace-pre-wrap break-words rounded-3xl rounded-br-lg bg-muted px-4 py-2.5 text-[15px] text-foreground leading-relaxed sm:text-sm\"\n                initial={{ opacity: 0, scale: 0.96, y: 10 }}\n                key={message.id}\n                layout=\"position\"\n                transition={{ type: \"spring\", stiffness: 420, damping: 34 }}\n              >\n                {message.text}\n              </motion.div>\n            ))}\n          </AnimatePresence>\n        </div>\n      ) : null}\n\n      <div className=\"@container relative rounded-[28px] border border-border bg-card shadow-[0_10px_26px_-22px_rgba(0,0,0,0.14)]\">\n        <AnimatePresence>\n          {waveRun > 0 ? (\n            <SendWave\n              key={`send-wave-${waveRun}`}\n              reducedMotion={reducedMotion}\n            />\n          ) : null}\n        </AnimatePresence>\n\n        <InputPrimitive\n          aria-label=\"Message\"\n          className=\"block max-h-[132px] w-full resize-none bg-transparent px-4 pt-4 pb-1 text-base text-foreground leading-6 outline-none placeholder:text-muted-foreground sm:px-5 sm:text-sm\"\n          onValueChange={(next) => {\n            setValue(next);\n            syncHeight();\n          }}\n          placeholder={placeholder}\n          render={\n            <textarea onKeyDown={handleKeyDown} ref={textareaRef} rows={1} />\n          }\n          value={value}\n        />\n\n        <div className=\"flex items-center gap-1.5 px-2.5 pt-1 pb-2.5 sm:gap-2 sm:px-3 sm:pb-3\">\n          {menuItems.length > 0 ? (\n            <PlusMenu\n              items={menuItems}\n              onSelect={onMenuSelect}\n              onToggle={onMenuToggle}\n            />\n          ) : (\n            <button\n              aria-label=\"Add attachment\"\n              className=\"flex size-8 shrink-0 items-center justify-center rounded-full text-foreground/70 transition-colors hover:text-foreground sm:size-9\"\n              onClick={onPlusClick}\n              type=\"button\"\n            >\n              <Plus aria-hidden=\"true\" className=\"size-4\" />\n            </button>\n          )}\n\n          {agents.length > 0 ? (\n            <OptionMenu\n              ariaLabel=\"Select agent\"\n              chipClassName=\"flex w-full min-w-14 items-center gap-1.5 rounded-full bg-muted px-3 py-1.5 font-medium text-foreground text-sm transition-colors hover:bg-accent sm:px-3.5 sm:py-2\"\n              onChange={(next) => {\n                setAgent(next);\n                onAgentChange?.(next);\n              }}\n              options={agents}\n              value={agent}\n            />\n          ) : null}\n\n          <div className=\"ml-auto flex shrink-0 items-center gap-1 sm:gap-1.5\">\n            {settingGroups.length > 0 || menuActions.length > 0 ? (\n              <SettingsDropdown\n                groups={settingGroups}\n                menuActions={menuActions}\n                onValueChange={(groupId, val) => {\n                  const next = { ...internalSettings, [groupId]: val };\n                  setInternalSettings(next);\n                  onSettingsChange?.(next);\n                }}\n                values={internalSettings}\n              />\n            ) : null}\n\n            <button\n              aria-label=\"Use voice input\"\n              className=\"@[21rem]:flex hidden size-8 shrink-0 items-center justify-center rounded-full text-foreground/60 transition-colors hover:text-foreground sm:size-9\"\n              onClick={onMicClick}\n              type=\"button\"\n            >\n              <Mic aria-hidden=\"true\" className=\"size-[18px]\" />\n            </button>\n\n            <button\n              aria-label=\"Send message\"\n              className={`flex size-8 shrink-0 items-center justify-center rounded-full transition-[background-color,opacity,transform] sm:size-9 ${hasValue ? \"bg-foreground text-background hover:opacity-90\" : \"cursor-default bg-muted-foreground/50 text-background\"}`}\n              disabled={!hasValue}\n              onClick={handleSend}\n              type=\"button\"\n            >\n              <ArrowUp\n                aria-hidden=\"true\"\n                className=\"size-4\"\n                strokeWidth={2.25}\n              />\n            </button>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "title": "AI Input",
  "description": "Chat-style AI composer with agent and model chips, sent-message bubbles, and an Apple Intelligence-style gradient wave on send."
}
