{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-input",
  "title": "AI Input",
  "description": "AI composer textarea with auto-grow, send/stop button morph, attachment slots, and Enter-to-send.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/ai-input.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type KeyboardEvent as ReactKeyboardEvent,\n  type ReactNode,\n} from 'react'\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { springs } from '@/lib/motion-tokens'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Switch } from '@/components/ui/switch'\n\nconst useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect\n\ninterface AiInputMenuCoordinatorValue {\n  openId: string | null\n  setOpenId: (id: string | null) => void\n}\n\nconst AiInputMenuCoordinatorContext = createContext<AiInputMenuCoordinatorValue | null>(null)\n\nfunction useCoordinatedOpen(): [boolean, (open: boolean) => void] {\n  const id = useId()\n  const coordinator = useContext(AiInputMenuCoordinatorContext)\n  const [standaloneOpen, setStandaloneOpen] = useState(false)\n\n  if (!coordinator) {\n    return [standaloneOpen, setStandaloneOpen]\n  }\n\n  const open = coordinator.openId === id\n  const setOpen = (next: boolean) => coordinator.setOpenId(next ? id : null)\n  return [open, setOpen]\n}\n\nconst ARROW_UP_PATH =\n  'M222.14,103.09,131.28,20.35a12,12,0,0,0-16.56,0L23.86,103.09A12,12,0,0,0,32,124a11.86,11.86,0,0,0,8.14-3.23L116,51.44V228a12,12,0,0,0,24,0V51.44l75.86,69.33A12,12,0,1,0,232,102.77Z'\n\nfunction SendIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 256 256\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      className={cn('size-4', className)}\n    >\n      <path d={ARROW_UP_PATH} />\n    </svg>\n  )\n}\n\nconst STOP_PATH =\n  'M180,20H76A56.06,56.06,0,0,0,20,76V180a56.06,56.06,0,0,0,56,56H180a56.06,56.06,0,0,0,56-56V76A56.06,56.06,0,0,0,180,20Z'\n\nfunction StopIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 256 256\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      className={cn('size-3.5', className)}\n    >\n      <path d={STOP_PATH} />\n    </svg>\n  )\n}\n\nconst MICROPHONE_PATH =\n  'M128,176a48.05,48.05,0,0,0,48-48V64a48,48,0,0,0-96,0v64A48.05,48.05,0,0,0,128,176ZM96,64a32,32,0,0,1,64,0v64a32,32,0,0,1-64,0Zm40,143.6V232a8,8,0,0,1-16,0V207.6A80.11,80.11,0,0,1,48,128a8,8,0,0,1,16,0,64,64,0,0,0,128,0,8,8,0,0,1,16,0A80.11,80.11,0,0,1,136,207.6Z'\n\nfunction MicIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 256 256\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      className={cn('size-4', className)}\n    >\n      <path d={MICROPHONE_PATH} />\n    </svg>\n  )\n}\n\nconst SEND_WAVE_GRADIENT =\n  'linear-gradient(180deg, transparent, color-mix(in oklch, var(--color-accent), transparent 88%), color-mix(in oklch, var(--color-accent), transparent 82%), transparent)'\n\nfunction SendWave({ reduceMotion }: { reduceMotion: boolean }) {\n  return (\n    <motion.div\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute inset-0 z-10 overflow-hidden rounded-[inherit]\"\n      initial={{ opacity: 1 }}\n      animate={{ opacity: 1 }}\n      exit={{ opacity: 0 }}\n      transition={{ duration: 0.25, ease: [0.22, 1, 0.36, 1] }}\n    >\n      <motion.div\n        className=\"absolute inset-x-0 top-0 h-[150%] blur-xl\"\n        style={{ background: SEND_WAVE_GRADIENT }}\n        initial={{ y: '55%' }}\n        animate={reduceMotion ? { y: '-17%' } : { y: '-105%' }}\n        transition={reduceMotion ? { duration: 0 } : { duration: 0.7, ease: [0.22, 1, 0.36, 1] }}\n      />\n    </motion.div>\n  )\n}\n\nexport type AiInputEffortLevel = 'low' | 'medium' | 'high'\n\nconst EFFORT_BAR_X = [1.5, 5.75, 10] as const\nconst EFFORT_BAR_WIDTH = 2.5\nconst EFFORT_BAR_BOTTOM = 12.5\nconst EFFORT_BAR_TALL = 10.5\nconst EFFORT_BAR_MID = 7.5\nconst EFFORT_BAR_SHORT = 4.5\n\nconst EFFORT_BAR_HEIGHTS: Record<AiInputEffortLevel, readonly [number, number, number]> = {\n  low: [3.25, 4.75, 0],\n  medium: [EFFORT_BAR_SHORT, EFFORT_BAR_MID, 0],\n  high: [EFFORT_BAR_SHORT, EFFORT_BAR_MID, EFFORT_BAR_TALL],\n}\n\nconst EFFORT_BAR_TRANSITION = { type: 'spring', stiffness: 380, damping: 34 } as const\n\nexport function AiInputEffortBarsIcon({\n  level,\n  className,\n}: {\n  level: AiInputEffortLevel\n  className?: string\n}) {\n  const heights = EFFORT_BAR_HEIGHTS[level]\n  return (\n    <svg\n      width={14}\n      height={14}\n      viewBox=\"0 0 14 14\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      className={className}\n    >\n      {heights.map((height, index) => {\n        const visible = height > 0\n        return (\n          <motion.rect\n            key={EFFORT_BAR_X[index]}\n            x={EFFORT_BAR_X[index]}\n            width={EFFORT_BAR_WIDTH}\n            rx={1}\n            fill=\"currentColor\"\n            initial={false}\n            animate={{\n              height: visible ? height : 0,\n              y: visible ? EFFORT_BAR_BOTTOM - height : EFFORT_BAR_BOTTOM,\n              opacity: visible ? 1 : 0,\n            }}\n            transition={EFFORT_BAR_TRANSITION}\n          />\n        )\n      })}\n    </svg>\n  )\n}\n\nconst PLUS_PATH =\n  'M216,128a8,8,0,0,1-8,8H136v72a8,8,0,0,1-16,0V136H48a8,8,0,0,1,0-16h72V48a8,8,0,0,1,16,0v72h72A8,8,0,0,1,216,128Z'\n\nfunction PlusIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 256 256\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      className={cn('size-4', className)}\n    >\n      <path d={PLUS_PATH} />\n    </svg>\n  )\n}\n\nexport type AiInputMenuItem =\n  | {\n      type: 'action'\n      value: string\n      label: string\n      icon?: ReactNode\n      shortcut?: string\n      disabled?: boolean\n      onSelect?: () => void\n      items?: undefined\n    }\n  | {\n      type: 'toggle'\n      value: string\n      label: string\n      icon?: ReactNode\n      checked: boolean\n      onCheckedChange: (checked: boolean) => void\n    }\n  | { type: 'separator'; value: string }\n  | {\n      type: 'submenu'\n      value: string\n      label: string\n      icon?: ReactNode\n      items: AiInputMenuItem[]\n    }\n\nexport interface AiInputPlusMenuProps {\n  items: AiInputMenuItem[]\n  label?: string\n  icon?: ReactNode\n  className?: string\n}\n\nconst CARET_RIGHT_PATH =\n  'M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z'\n\nfunction ChevronRightIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 256 256\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      className={cn('size-3.5', className)}\n    >\n      <path d={CARET_RIGHT_PATH} />\n    </svg>\n  )\n}\n\nconst CARET_LEFT_PATH =\n  'M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z'\n\nfunction ChevronLeftIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 256 256\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      className={cn('size-3.5', className)}\n    >\n      <path d={CARET_LEFT_PATH} />\n    </svg>\n  )\n}\n\nconst MENU_SLIDE_TRANSITION = { duration: 0.18, ease: [0.22, 1, 0.36, 1] } as const\n\nconst menuSlideVariants = {\n  enter: (dir: number) => ({ opacity: 0, x: dir * 12 }),\n  visible: { opacity: 1, x: 0 },\n  exit: (dir: number) => ({ opacity: 0, x: dir * -12 }),\n}\n\nfunction MenuBackRow({ label, onClick }: { label: string; onClick: () => void }) {\n  return (\n    <button\n      type=\"button\"\n      className={cn(\n        'relative flex min-h-11 w-full cursor-pointer items-center gap-2 rounded-md px-3 py-2.5 text-start text-sm font-medium text-(--color-fg) outline-none',\n        'hover:bg-(--color-surface-2) focus-visible:bg-(--color-surface-2)',\n        'transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n      )}\n      onClick={onClick}\n      onPointerDown={(e) => e.preventDefault()}\n    >\n      <ChevronLeftIcon className=\"shrink-0 text-(--color-muted)\" />\n      {label}\n    </button>\n  )\n}\n\nfunction PlusMenuRow({\n  item,\n  onNavigate,\n}: {\n  item: AiInputMenuItem\n  onNavigate: (submenu: { label: string; items: AiInputMenuItem[] }) => void\n}) {\n  if (item.type === 'separator') {\n    return <DropdownMenuSeparator />\n  }\n\n  if (item.type === 'toggle') {\n    return (\n      <div className=\"flex min-h-11 w-full items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm text-(--color-fg)\">\n        <span className=\"flex min-w-0 flex-1 items-center gap-2 truncate\">\n          {item.icon && (\n            <span aria-hidden=\"true\" className=\"shrink-0 text-(--color-muted)\">\n              {item.icon}\n            </span>\n          )}\n          {item.label}\n        </span>\n        <Switch checked={item.checked} onCheckedChange={item.onCheckedChange} />\n      </div>\n    )\n  }\n\n  if (item.type === 'submenu') {\n    return (\n      <button\n        type=\"button\"\n        className={cn(\n          'relative flex min-h-11 w-full cursor-pointer items-center justify-between gap-3 rounded-md px-3 py-2.5 text-start text-sm text-(--color-fg) outline-none',\n          'hover:bg-(--color-surface-2) focus-visible:bg-(--color-surface-2)',\n          'transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n        )}\n        onClick={() => onNavigate({ label: item.label, items: item.items })}\n        onPointerDown={(e) => e.preventDefault()}\n      >\n        <span className=\"flex min-w-0 flex-1 items-center gap-2 truncate\">\n          {item.icon && (\n            <span aria-hidden=\"true\" className=\"shrink-0 text-(--color-muted)\">\n              {item.icon}\n            </span>\n          )}\n          {item.label}\n        </span>\n        <ChevronRightIcon className=\"shrink-0 text-(--color-muted)\" />\n      </button>\n    )\n  }\n\n  return (\n    <DropdownMenuItem disabled={item.disabled} onClick={item.onSelect} textValue={item.label}>\n      {item.icon && (\n        <span aria-hidden=\"true\" className=\"shrink-0 text-(--color-muted)\">\n          {item.icon}\n        </span>\n      )}\n      <span className=\"flex-1 truncate\">{item.label}</span>\n      {item.shortcut && (\n        <span className=\"shrink-0 text-xs tabular-nums text-(--color-muted)\">{item.shortcut}</span>\n      )}\n    </DropdownMenuItem>\n  )\n}\n\nexport function AiInputPlusMenu({\n  items,\n  label = 'More actions',\n  icon,\n  className,\n}: AiInputPlusMenuProps) {\n  const [submenu, setSubmenu] = useState<{ label: string; items: AiInputMenuItem[] } | null>(null)\n  const [slideDir, setSlideDir] = useState(1)\n  const [open, setOpen] = useCoordinatedOpen()\n\n  const navigateTo = useCallback((next: { label: string; items: AiInputMenuItem[] }) => {\n    setSlideDir(1)\n    setSubmenu(next)\n  }, [])\n\n  const navigateBack = useCallback(() => {\n    setSlideDir(-1)\n    setSubmenu(null)\n  }, [])\n\n  const activeItems = submenu?.items ?? items\n\n  return (\n    <DropdownMenu\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next)\n        if (!next) setSubmenu(null)\n      }}\n    >\n      <DropdownMenuTrigger\n        showChevron={false}\n        aria-label={label}\n        style={{}}\n        className={cn(\n          'inline-flex size-8 shrink-0 min-h-0 w-8 items-center justify-center rounded-full supports-[corner-shape:squircle]:corner-squircle border-0 bg-transparent p-0 text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-fg)',\n          className,\n        )}\n      >\n        <span className=\"flex w-full items-center justify-center\">{icon ?? <PlusIcon />}</span>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent className=\"overflow-hidden\">\n        <AnimatePresence mode=\"popLayout\" initial={false} custom={slideDir}>\n          <motion.div\n            key={submenu?.label ?? 'root'}\n            custom={slideDir}\n            variants={menuSlideVariants}\n            initial=\"enter\"\n            animate=\"visible\"\n            exit=\"exit\"\n            transition={MENU_SLIDE_TRANSITION}\n          >\n            {submenu && <MenuBackRow label={submenu.label} onClick={navigateBack} />}\n            {activeItems.map((item) => (\n              <PlusMenuRow key={item.value} item={item} onNavigate={navigateTo} />\n            ))}\n          </motion.div>\n        </AnimatePresence>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n\nexport interface AiInputAgentOption {\n  value: string\n  label: string\n}\n\nexport interface AiInputAgentMenuProps {\n  options: AiInputAgentOption[]\n  value: string\n  onValueChange: (value: string) => void\n  label?: string\n  className?: string\n}\n\nexport function AiInputAgentMenu({\n  options,\n  value,\n  onValueChange,\n  label = 'Select agent',\n  className,\n}: AiInputAgentMenuProps) {\n  const selected = options.find((option) => option.value === value) ?? options[0]\n  const [open, setOpen] = useCoordinatedOpen()\n\n  return (\n    <DropdownMenu open={open} onOpenChange={setOpen}>\n      <DropdownMenuTrigger\n        showChevron={false}\n        aria-label={label}\n        style={{ width: 'auto' }}\n        className={cn(\n          'inline-flex min-h-0 w-auto items-center gap-1.5 rounded-full border-0 bg-(--color-surface-2) px-3 py-1.5 text-sm font-medium text-(--color-fg) hover:bg-(--color-border)',\n          className,\n        )}\n      >\n        <span className=\"truncate\">{selected?.label}</span>\n        <ChevronRightIcon className=\"shrink-0 rotate-90 text-(--color-muted)\" />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\">\n        <DropdownMenuGroup>\n          {options.map((option) => (\n            <SettingOptionRow\n              key={option.value}\n              option={option}\n              selected={option.value === selected?.value}\n              onSelect={() => onValueChange(option.value)}\n            />\n          ))}\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n\nexport interface AiInputSettingOption {\n  value: string\n  label: string\n  description?: string\n}\n\nexport interface AiInputSettingGroup {\n  id: string\n  label: string\n  display?: 'inline' | 'submenu' | 'featured'\n  options: AiInputSettingOption[]\n}\n\nexport interface AiInputSettingsDropdownProps {\n  groups: AiInputSettingGroup[]\n  values: Record<string, string>\n  onValueChange: (groupId: string, value: string) => void\n  effortLevel?: AiInputEffortLevel\n  label?: string\n  className?: string\n}\n\nconst CHECK_PATH =\n  'M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z'\n\nfunction CheckIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 256 256\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n      className={cn('size-4', className)}\n    >\n      <path d={CHECK_PATH} />\n    </svg>\n  )\n}\n\nfunction SettingOptionRow({\n  option,\n  selected,\n  onSelect,\n}: {\n  option: AiInputSettingOption\n  selected: boolean\n  onSelect: () => void\n}) {\n  return (\n    <DropdownMenuItem textValue={option.label} onClick={onSelect}>\n      <span className=\"min-w-0 flex-1\">\n        <span className=\"block truncate\">{option.label}</span>\n        {option.description && (\n          <span className=\"block truncate text-xs text-(--color-muted)\">{option.description}</span>\n        )}\n      </span>\n      {selected && <CheckIcon className=\"shrink-0 text-(--color-accent)\" />}\n    </DropdownMenuItem>\n  )\n}\n\nfunction FeaturedSettingRow({ option }: { option: AiInputSettingOption }) {\n  return (\n    <div className=\"flex items-start justify-between gap-2.5 px-3 py-2.5\">\n      <div className=\"min-w-0\">\n        <p className=\"truncate text-sm font-medium text-(--color-fg)\">{option.label}</p>\n        {option.description && (\n          <p className=\"mt-0.5 truncate text-xs text-(--color-muted)\">{option.description}</p>\n        )}\n      </div>\n      <CheckIcon className=\"mt-0.5 shrink-0 text-(--color-accent)\" />\n    </div>\n  )\n}\n\nexport function AiInputSettingsDropdown({\n  groups,\n  values,\n  onValueChange,\n  effortLevel,\n  label = 'Settings',\n  className,\n}: AiInputSettingsDropdownProps) {\n  const [submenu, setSubmenu] = useState<AiInputSettingGroup | null>(null)\n  const [open, setOpen] = useCoordinatedOpen()\n\n  const selectedLabels = groups.map(\n    (group) => group.options.find((o) => o.value === values[group.id])?.label,\n  )\n  const triggerLabel = selectedLabels.filter(Boolean).join(', ')\n\n  const featuredGroups = groups.filter((group) => group.display === 'featured')\n  const inlineGroups = groups.filter((group) => !group.display || group.display === 'inline')\n  const submenuGroups = groups.filter((group) => group.display === 'submenu')\n\n  return (\n    <DropdownMenu\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next)\n        if (!next) setSubmenu(null)\n      }}\n    >\n      <DropdownMenuTrigger\n        showChevron={false}\n        aria-label={`${label}: ${triggerLabel}`}\n        style={{ width: 'auto' }}\n        className={cn(\n          'inline-flex min-h-0 w-auto items-center gap-1.5 rounded-full border-0 bg-transparent px-2 py-1 text-sm text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-fg)',\n          className,\n        )}\n      >\n        {effortLevel && <AiInputEffortBarsIcon level={effortLevel} className=\"shrink-0\" />}\n        {groups.map((group, index) => {\n          const optionLabel = selectedLabels[index]\n          if (!optionLabel) return null\n          return (\n            <span\n              key={group.id}\n              className={cn('truncate', index === 0 ? 'font-medium text-(--color-fg)' : '')}\n            >\n              {optionLabel}\n            </span>\n          )\n        })}\n        <ChevronRightIcon className=\"shrink-0 rotate-90 text-(--color-muted)\" />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\" className=\"overflow-hidden\">\n        <AnimatePresence mode=\"popLayout\" initial={false} custom={submenu ? 1 : -1}>\n          <motion.div\n            key={submenu?.id ?? 'root'}\n            custom={submenu ? 1 : -1}\n            variants={menuSlideVariants}\n            initial=\"enter\"\n            animate=\"visible\"\n            exit=\"exit\"\n            transition={MENU_SLIDE_TRANSITION}\n          >\n            {submenu ? (\n              <>\n                <MenuBackRow label={submenu.label} onClick={() => setSubmenu(null)} />\n                {submenu.options.map((option) => (\n                  <SettingOptionRow\n                    key={option.value}\n                    option={option}\n                    selected={values[submenu.id] === option.value}\n                    onSelect={() => onValueChange(submenu.id, option.value)}\n                  />\n                ))}\n              </>\n            ) : (\n              <>\n                {featuredGroups.map((group) => {\n                  const selected = group.options.find((o) => o.value === values[group.id])\n                  return selected ? <FeaturedSettingRow key={group.id} option={selected} /> : null\n                })}\n                {featuredGroups.length > 0 && inlineGroups.length > 0 && <DropdownMenuSeparator />}\n                {inlineGroups.map((group, index) => (\n                  <DropdownMenuGroup key={group.id} label={index === 0 ? undefined : group.label}>\n                    {group.options.map((option) => (\n                      <SettingOptionRow\n                        key={option.value}\n                        option={option}\n                        selected={values[group.id] === option.value}\n                        onSelect={() => onValueChange(group.id, option.value)}\n                      />\n                    ))}\n                  </DropdownMenuGroup>\n                ))}\n                {submenuGroups.length > 0 &&\n                  (featuredGroups.length > 0 || inlineGroups.length > 0) && (\n                    <DropdownMenuSeparator />\n                  )}\n                {submenuGroups.map((group) => {\n                  const selectedLabel = group.options.find(\n                    (o) => o.value === values[group.id],\n                  )?.label\n                  return (\n                    <button\n                      key={group.id}\n                      type=\"button\"\n                      className={cn(\n                        'relative flex min-h-11 w-full cursor-pointer items-center justify-between gap-3 rounded-md px-3 py-2.5 text-start text-sm text-(--color-fg) outline-none',\n                        'hover:bg-(--color-surface-2) focus-visible:bg-(--color-surface-2)',\n                        'transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n                      )}\n                      onClick={() => setSubmenu(group)}\n                      onPointerDown={(e) => e.preventDefault()}\n                    >\n                      <span className=\"truncate\">{group.label}</span>\n                      <span className=\"flex shrink-0 items-center gap-1 text-(--color-muted)\">\n                        {selectedLabel && <span className=\"truncate\">{selectedLabel}</span>}\n                        <ChevronRightIcon />\n                      </span>\n                    </button>\n                  )\n                })}\n              </>\n            )}\n          </motion.div>\n        </AnimatePresence>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n\nfunction useControlledState(\n  controlledValue: string | undefined,\n  defaultValue: string | undefined,\n  onValueChange: ((value: string) => void) | undefined,\n): [string, (next: string) => void] {\n  const [internal, setInternal] = useState(defaultValue ?? '')\n  const isControlled = controlledValue !== undefined\n  const value = isControlled ? controlledValue : internal\n\n  const setValue = useCallback(\n    (next: string) => {\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n    },\n    [isControlled, onValueChange],\n  )\n\n  return [value, setValue]\n}\n\nexport interface AiInputMessage {\n  id: number\n  text: string\n}\n\nexport interface AiInputProps {\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  onSubmit?: (value: string) => void\n  streaming?: boolean\n  onStop?: () => void\n  placeholder?: string\n  maxRows?: number\n  disabled?: boolean\n  name?: string\n  startSlot?: ReactNode\n  endSlot?: ReactNode\n  showMessages?: boolean\n  onMicClick?: () => void\n  className?: string\n}\n\nexport const AiInput = forwardRef<HTMLTextAreaElement, AiInputProps>(\n  (\n    {\n      value: controlledValue,\n      defaultValue,\n      onValueChange,\n      onSubmit,\n      streaming = false,\n      onStop,\n      placeholder = 'Type a message…',\n      maxRows = 6,\n      disabled = false,\n      name,\n      startSlot,\n      endSlot,\n      showMessages = false,\n      onMicClick,\n      className,\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion()\n    const internalRef = useRef<HTMLTextAreaElement>(null)\n    const messagesRef = useRef<HTMLDivElement>(null)\n    const [launched, setLaunched] = useState(false)\n    const launchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n    const [messages, setMessages] = useState<AiInputMessage[]>([])\n    const nextMessageIdRef = useRef(0)\n\n    const setRefs = useCallback(\n      (el: HTMLTextAreaElement | null) => {\n        ;(internalRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = el\n        if (typeof ref === 'function') ref(el)\n        else if (ref) (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = el\n      },\n      [ref],\n    )\n\n    const [value, setValue] = useControlledState(controlledValue, defaultValue, onValueChange)\n\n    useIsoLayoutEffect(() => {\n      const el = internalRef.current\n      if (!el) return\n      el.style.height = 'auto'\n      const computed = getComputedStyle(el)\n      const lineHeight = parseFloat(computed.lineHeight)\n      if (Number.isNaN(lineHeight)) return\n      const max = lineHeight * maxRows\n      const next = Math.min(el.scrollHeight, max)\n      el.style.height = `${next}px`\n      el.style.overflowY = el.scrollHeight > max ? 'auto' : 'hidden'\n    }, [value, maxRows])\n\n    const trimmed = value.trim()\n    const canSend = !disabled && !streaming && trimmed.length > 0\n\n    const handleSubmit = useCallback(() => {\n      if (!canSend) return\n\n      if (showMessages) {\n        nextMessageIdRef.current += 1\n        setMessages((prev) => [...prev, { id: nextMessageIdRef.current, text: trimmed }])\n        requestAnimationFrame(() => {\n          messagesRef.current?.scrollTo({\n            top: messagesRef.current.scrollHeight,\n            behavior: reduceMotion ? 'auto' : 'smooth',\n          })\n        })\n      }\n\n      if (!reduceMotion) {\n        setLaunched(true)\n        launchTimerRef.current = setTimeout(() => {\n          setLaunched(false)\n        }, 400)\n      }\n\n      onSubmit?.(trimmed)\n      setValue('')\n    }, [canSend, onSubmit, trimmed, reduceMotion, showMessages, setValue])\n\n    const handleStop = useCallback(() => {\n      onStop?.()\n    }, [onStop])\n\n    const handleKeyDown = useCallback(\n      (e: ReactKeyboardEvent<HTMLTextAreaElement>) => {\n        if (e.nativeEvent.isComposing) return\n        if (e.key === 'Enter' && !e.shiftKey) {\n          e.preventDefault()\n          if (streaming) {\n            handleStop()\n          } else {\n            handleSubmit()\n          }\n        }\n      },\n      [streaming, handleSubmit, handleStop],\n    )\n\n    useEffect(() => {\n      return () => {\n        if (launchTimerRef.current !== null) {\n          clearTimeout(launchTimerRef.current)\n        }\n      }\n    }, [])\n\n    const showStop = streaming\n    const buttonDisabled = !streaming && !canSend\n\n    const [openMenuId, setOpenMenuId] = useState<string | null>(null)\n    const coordinatorValue = useMemo(\n      () => ({ openId: openMenuId, setOpenId: setOpenMenuId }),\n      [openMenuId],\n    )\n\n    return (\n      <AiInputMenuCoordinatorContext.Provider value={coordinatorValue}>\n        <div className={cn('w-full', className)}>\n          {showMessages && messages.length > 0 && (\n            <div\n              ref={messagesRef}\n              className=\"mb-3 flex max-h-72 flex-col gap-2 overflow-y-auto overscroll-contain px-1 py-1 scrollbar-gutter-stable\"\n            >\n              <AnimatePresence initial={false}>\n                {messages.map((message) => (\n                  <motion.div\n                    key={message.id}\n                    layout=\"position\"\n                    initial={reduceMotion ? false : { opacity: 0, scale: 0.96, y: 10 }}\n                    animate={{ opacity: 1, scale: 1, y: 0 }}\n                    transition={\n                      reduceMotion\n                        ? { duration: 0 }\n                        : { type: 'spring', stiffness: 420, damping: 34 }\n                    }\n                    className=\"max-w-[85%] self-end rounded-3xl rounded-br-lg bg-(--color-surface-2) px-4 py-2.5 text-sm leading-relaxed wrap-break-word whitespace-pre-wrap text-foreground\"\n                  >\n                    {message.text}\n                  </motion.div>\n                ))}\n              </AnimatePresence>\n            </div>\n          )}\n\n          <div\n            className={cn(\n              'relative rounded-lg supports-[corner-shape:squircle]:corner-squircle border border-(--color-border) bg-(--color-card)',\n              'transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n              'focus-within:border-(--color-accent)',\n              disabled && 'opacity-50 pointer-events-none',\n            )}\n          >\n            <AnimatePresence>\n              {launched && <SendWave reduceMotion={!!reduceMotion} />}\n            </AnimatePresence>\n\n            <textarea\n              ref={setRefs}\n              value={value}\n              onChange={(e) => setValue(e.target.value)}\n              onKeyDown={handleKeyDown}\n              placeholder={placeholder}\n              disabled={disabled}\n              name={name}\n              rows={1}\n              aria-label={placeholder}\n              className={cn(\n                'block max-h-40 w-full min-h-6 resize-none bg-transparent px-3.5 pt-3 pb-1 text-sm text-foreground outline-none',\n                'placeholder:text-muted-foreground',\n              )}\n              style={{ fieldSizing: 'content' } as React.CSSProperties}\n            />\n\n            <div className=\"flex items-center gap-1.5 px-2.5 pt-1 pb-2.5\">\n              {startSlot}\n\n              {endSlot}\n\n              <div className=\"ms-auto flex shrink-0 items-center gap-1\">\n                {onMicClick && (\n                  <button\n                    type=\"button\"\n                    onClick={onMicClick}\n                    aria-label=\"Use voice input\"\n                    className={cn(\n                      'inline-flex size-8 shrink-0 items-center justify-center rounded-full border-0 bg-transparent text-(--color-muted) outline-none',\n                      'hover:bg-(--color-surface-2) hover:text-(--color-fg)',\n                      'transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n                    )}\n                  >\n                    <MicIcon />\n                  </button>\n                )}\n\n                <button\n                  type=\"button\"\n                  onClick={showStop ? handleStop : handleSubmit}\n                  onPointerDown={(e) => e.preventDefault()}\n                  disabled={buttonDisabled}\n                  aria-label={showStop ? 'Stop generation' : 'Send message'}\n                  className={cn(\n                    'relative flex shrink-0 items-center justify-center size-8 rounded-full squircle cursor-pointer outline-none',\n                    'bg-foreground text-background',\n                    'transition-[opacity,transform] duration-(--motion-dur-fast) motion-reduce:transition-none',\n                    'active:scale-95 motion-reduce:active:scale-100',\n                    'disabled:opacity-40 disabled:pointer-events-none',\n                    'focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 focus-visible:ring-offset-background',\n                  )}\n                >\n                  <AnimatePresence mode=\"wait\" initial={false}>\n                    {showStop ? (\n                      <motion.span\n                        key=\"stop\"\n                        className=\"flex items-center justify-center\"\n                        initial={reduceMotion ? false : { opacity: 0, scale: 0.9 }}\n                        animate={{ opacity: 1, scale: 1 }}\n                        exit={reduceMotion ? undefined : { opacity: 0, scale: 0.9 }}\n                        transition={reduceMotion ? { duration: 0 } : { duration: 0.12 }}\n                      >\n                        <StopIcon />\n                      </motion.span>\n                    ) : (\n                      <motion.span\n                        key=\"send\"\n                        className=\"flex items-center justify-center\"\n                        initial={reduceMotion ? false : { opacity: 0, scale: 0.9 }}\n                        animate={\n                          launched && !reduceMotion\n                            ? { opacity: 0, y: -8, scale: 0.9 }\n                            : { opacity: 1, y: 0, scale: 1 }\n                        }\n                        exit={reduceMotion ? undefined : { opacity: 0, scale: 0.9 }}\n                        transition={\n                          reduceMotion\n                            ? { duration: 0 }\n                            : launched\n                              ? { duration: 0.25, ease: [0.22, 1, 0.36, 1] }\n                              : springs.fast\n                        }\n                      >\n                        <SendIcon />\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </AiInputMenuCoordinatorContext.Provider>\n    )\n  },\n)\n\nAiInput.displayName = 'AiInput'\n\nexport function AiInputPreview() {\n  const [value, setValue] = useState('')\n  const [webSearch, setWebSearch] = useState(false)\n  const [agent, setAgent] = useState('claude')\n  const [settings, setSettings] = useState<Record<string, string>>({\n    model: 'fast',\n    effort: 'medium',\n  })\n\n  return (\n    <div className=\"flex h-full w-full items-center justify-center p-6\">\n      <div className=\"w-full max-w-md\">\n        <AiInput\n          value={value}\n          onValueChange={setValue}\n          onSubmit={() => setValue('')}\n          placeholder=\"Ask anything...\"\n          showMessages\n          onMicClick={() => {}}\n          startSlot={\n            <AiInputPlusMenu\n              items={[\n                { type: 'action', value: 'attach', label: 'Attach file' },\n                { type: 'action', value: 'photo', label: 'Add photo' },\n                { type: 'separator', value: 'sep-1' },\n                {\n                  type: 'toggle',\n                  value: 'web-search',\n                  label: 'Web search',\n                  checked: webSearch,\n                  onCheckedChange: setWebSearch,\n                },\n                { type: 'separator', value: 'sep-2' },\n                {\n                  type: 'submenu',\n                  value: 'connect',\n                  label: 'Connect apps',\n                  items: [\n                    { type: 'action', value: 'drive', label: 'Google Drive' },\n                    { type: 'action', value: 'notion', label: 'Notion' },\n                    { type: 'action', value: 'github', label: 'GitHub' },\n                  ],\n                },\n              ]}\n            />\n          }\n          endSlot={\n            <>\n              <AiInputAgentMenu\n                options={[\n                  { value: 'claude', label: 'Claude' },\n                  { value: 'assistant', label: 'Assistant' },\n                ]}\n                value={agent}\n                onValueChange={setAgent}\n              />\n              <AiInputSettingsDropdown\n                className=\"ms-auto\"\n                groups={[\n                  {\n                    id: 'model',\n                    label: 'Model',\n                    display: 'featured',\n                    options: [\n                      { value: 'fast', label: 'Fast', description: 'Quick, everyday answers' },\n                      {\n                        value: 'thinking',\n                        label: 'Thinking',\n                        description: 'Slower, more thorough',\n                      },\n                    ],\n                  },\n                  {\n                    id: 'effort',\n                    label: 'Effort',\n                    display: 'submenu',\n                    options: [\n                      { value: 'low', label: 'Low' },\n                      { value: 'medium', label: 'Medium' },\n                      { value: 'high', label: 'High' },\n                    ],\n                  },\n                ]}\n                values={settings}\n                onValueChange={(groupId, val) =>\n                  setSettings((prev) => ({ ...prev, [groupId]: val }))\n                }\n                effortLevel={settings.effort as AiInputEffortLevel}\n              />\n            </>\n          }\n        />\n      </div>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/motion-tokens.ts",
      "type": "registry:lib",
      "content": "export const durations = {\n  instant: '50ms',\n  fast: '120ms',\n  base: '200ms',\n  slow: '320ms',\n} as const\n\nexport const easings = {\n  out: 'cubic-bezier(0.22, 1, 0.36, 1)',\n  inOut: 'cubic-bezier(0.65, 0, 0.35, 1)',\n} as const\n\nexport const springs = {\n  fast: { type: 'spring', stiffness: 400, damping: 30, mass: 0.8 },\n  press: { type: 'spring', stiffness: 700, damping: 32, mass: 1 },\n  moderate: { type: 'spring', stiffness: 300, damping: 24, mass: 1 },\n  settle: { type: 'spring', stiffness: 260, damping: 26, mass: 1 },\n} as const\n"
    }
  ],
  "type": "registry:ui"
}
