{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prompt-composer",
  "title": "Prompt Composer",
  "description": "A compound prompt bar with composable add, input, and action regions.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dropdown-menu",
    "textarea",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/prompt-composer.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  ArrowUp,\n  Check,\n  ChevronDown,\n  FileText,\n  LoaderCircle,\n  Paperclip,\n  Plus,\n  X,\n} from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type Dispatch,\n  type FormEvent,\n  type KeyboardEvent,\n  type ReactNode,\n  type SetStateAction,\n  useContext,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport type PromptComposerModel = {\n  id: string;\n  label: string;\n};\n\nexport const PROMPT_COMPOSER_VARIANTS = [\"rounded\", \"pill\", \"ghost\", \"compact\"] as const;\n\nexport type PromptComposerVariant = (typeof PROMPT_COMPOSER_VARIANTS)[number];\n\nexport type PromptComposerProps = Omit<ComponentProps<\"form\">, \"onSubmit\" | \"onChange\"> & {\n  variant?: PromptComposerVariant;\n  busy?: boolean;\n  disabled?: boolean;\n  invalid?: boolean;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  onSubmit?: (prompt: string, files: File[]) => void | Promise<void>;\n};\n\ntype Attachment = { id: string; file: File };\ntype OpenMenu = \"add\" | \"model\" | null;\n\nconst promptComposerCardVariants = cva(\n  \"relative isolate flex flex-col border bg-card transition-colors duration-150 focus-within:border-border-strong\",\n  {\n    variants: {\n      variant: {\n        rounded: \"gap-1.5 rounded-[14px] p-1.5\",\n        pill: \"gap-1 p-1\",\n        ghost: \"gap-1.5 rounded-[14px] border-transparent bg-transparent p-1\",\n        compact: \"gap-1 rounded-xl p-1\",\n      },\n      open: { true: \"\", false: \"\" },\n      invalid: { true: \"border-destructive focus-within:border-destructive\", false: \"\" },\n    },\n    compoundVariants: [\n      { variant: \"pill\", open: false, className: \"rounded-full\" },\n      { variant: \"pill\", open: true, className: \"rounded-3xl\" },\n    ],\n  },\n);\n\nfunction composerChrome(variant: PromptComposerVariant) {\n  const compact = variant === \"compact\";\n  const pill = variant === \"pill\";\n\n  return {\n    compact,\n    pill,\n    controlSize: compact ? 24 : 28,\n    controlClass: compact ? \"size-6\" : \"size-7\",\n    controlHeightClass: compact ? \"h-6\" : \"h-7\",\n    controlRadiusClass: pill ? \"rounded-full\" : compact ? \"rounded-[6px]\" : \"rounded-lg\",\n    chipClass: cn(\n      compact ? \"h-5.5 text-[11px]\" : \"h-6.5 text-[11.5px]\",\n      pill ? \"rounded-full\" : compact ? \"rounded-[5px]\" : \"rounded-md\",\n    ),\n    fieldClass: compact\n      ? \"min-h-6 py-1 text-[12.5px]/4 md:text-[12.5px]/4\"\n      : \"min-h-7 py-1.25 text-[13px]/[18px] md:text-[13px]/[18px]\",\n    maxFieldHeightClass: compact ? \"max-h-20\" : \"max-h-[100px]\",\n    modelClass: compact ? \"text-[11px]\" : \"text-xs\",\n    iconClass: compact ? \"size-3.5\" : \"size-4\",\n  };\n}\n\ntype PromptComposerContextValue = {\n  prompt: string;\n  setPrompt: (value: string) => void;\n  attachments: Attachment[];\n  addFiles: (files: File[]) => void;\n  removeAttachment: (id: string) => void;\n  busy: boolean;\n  invalid: boolean;\n  locked: boolean;\n  canSend: boolean;\n  expanded: boolean;\n  openMenu: OpenMenu;\n  setOpenMenu: Dispatch<SetStateAction<OpenMenu>>;\n  inputId: string;\n  controlsRef: React.RefObject<HTMLDivElement | null>;\n  addRef: React.RefObject<HTMLDivElement | null>;\n  actionsRef: React.RefObject<HTMLDivElement | null>;\n  inputRef: React.RefObject<HTMLTextAreaElement | null>;\n  /** Set while a closed menu hands focus back to its trigger, which forwards it to the input. */\n  focusInputOnReturnRef: React.RefObject<boolean>;\n  /** Set while the file picker is up, so the menu (and its file input) stays mounted. */\n  keepMenuOpenRef: React.RefObject<boolean>;\n  chrome: ReturnType<typeof composerChrome>;\n};\n\nconst PromptComposerContext = createContext<PromptComposerContextValue | null>(null);\n\nfunction usePromptComposer(name: string) {\n  const context = useContext(PromptComposerContext);\n  if (!context) throw new Error(`${name} must be used within PromptComposer`);\n  return context;\n}\n\nfunction menuOpenChange(context: PromptComposerContextValue, menu: Exclude<OpenMenu, null>) {\n  return (open: boolean) => {\n    if (!open && context.keepMenuOpenRef.current) return;\n    context.setOpenMenu((current) => (open ? menu : current === menu ? null : current));\n  };\n}\n\n/** Clears a flag on the next key or pointer press anywhere, so a pending hand-off can't go stale. */\nfunction expireOnNextInput(flag: React.RefObject<boolean>) {\n  const expire = () => {\n    flag.current = false;\n    document.removeEventListener(\"keydown\", expire, true);\n    document.removeEventListener(\"pointerdown\", expire, true);\n  };\n  document.addEventListener(\"keydown\", expire, true);\n  document.addEventListener(\"pointerdown\", expire, true);\n  return expire;\n}\n\n/**\n * Menu triggers forward the focus their menu returns on close to the input. Radix and Base UI\n * both restore focus to the trigger, so this works on either without primitive-specific props.\n */\nfunction forwardReturnedFocus(context: PromptComposerContextValue) {\n  return () => {\n    if (!context.focusInputOnReturnRef.current) return;\n    context.focusInputOnReturnRef.current = false;\n    context.inputRef.current?.focus();\n  };\n}\n\nfunction FloatingMenu({ kind, children }: { kind: \"sources\" | \"models\"; children: ReactNode }) {\n  usePromptComposer(\"PromptComposerMenu\");\n\n  return (\n    <DropdownMenuContent\n      data-slot=\"prompt-composer-menu\"\n      side=\"top\"\n      align={kind === \"sources\" ? \"start\" : \"end\"}\n      sideOffset={8}\n      className={cn(\n        \"min-w-0 rounded-[14px] border-0 p-1 shadow-[0_0_0_1px_var(--border-strong),0_10px_28px_color-mix(in_oklab,black_42%,transparent)]\",\n        \"duration-180 ease-out-quint data-[side=top]:slide-in-from-bottom-0 data-[state=open]:zoom-in-96 data-open:zoom-in-96 motion-reduce:animate-none\",\n        kind === \"sources\" ? \"w-[min(340px,calc(100vw-32px))]\" : \"w-44\",\n      )}\n    >\n      {children}\n    </DropdownMenuContent>\n  );\n}\n\nconst promptComposerMenuItemClass =\n  \"group/item min-h-9 w-full cursor-pointer gap-2.5 rounded-[10px] px-2 py-1.5 text-left text-[13px]/[18px] transition-colors duration-120 ease-out data-[disabled]:cursor-not-allowed motion-reduce:transition-none [&_svg:not([class*='text-'])]:text-current\";\n\nexport function PromptComposer({\n  variant = \"rounded\",\n  busy = false,\n  disabled = false,\n  invalid = false,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onSubmit,\n  className,\n  children,\n  ...props\n}: PromptComposerProps) {\n  const [internalPrompt, setInternalPrompt] = useState(defaultValue);\n  const [attachments, setAttachments] = useState<Attachment[]>([]);\n  const [openMenu, setOpenMenu] = useState<OpenMenu>(null);\n  const [expanded, setExpanded] = useState(false);\n  const attachmentId = useRef(0);\n  const inputId = useId();\n  const controlsRef = useRef<HTMLDivElement>(null);\n  const addRef = useRef<HTMLDivElement>(null);\n  const actionsRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const focusInputOnReturnRef = useRef(false);\n  const keepMenuOpenRef = useRef(false);\n  const previousMenuRef = useRef<OpenMenu>(null);\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const prompt = value ?? internalPrompt;\n  const locked = busy || disabled;\n  const canSend = !locked && (prompt.trim().length > 0 || attachments.length > 0);\n  const chrome = composerChrome(variant);\n\n  const setPrompt = (nextValue: string) => {\n    if (value === undefined) setInternalPrompt(nextValue);\n    onValueChange?.(nextValue);\n  };\n\n  useLayoutEffect(() => {\n    const controls = controlsRef.current;\n    const measure = measureRef.current;\n    if (!controls || !measure) return;\n\n    const reservedWidth =\n      (addRef.current?.offsetWidth ?? chrome.controlSize) +\n      (actionsRef.current?.offsetWidth ?? chrome.controlSize) +\n      8;\n    const inlineInputWidth = controls.clientWidth - reservedWidth;\n    const needsFullWidth = prompt.includes(\"\\n\") || measure.offsetWidth + 8 > inlineInputWidth;\n    if (needsFullWidth !== expanded) setExpanded(needsFullWidth);\n  }, [prompt, expanded, chrome.controlSize]);\n\n  useEffect(() => {\n    const closed = previousMenuRef.current !== null && openMenu === null;\n    previousMenuRef.current = openMenu;\n    if (!closed) return;\n    focusInputOnReturnRef.current = true;\n    const expire = expireOnNextInput(focusInputOnReturnRef);\n    return expire;\n  }, [openMenu]);\n\n  const submit = async (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    if (!canSend) return;\n    await onSubmit?.(\n      prompt.trim(),\n      attachments.map((item) => item.file),\n    );\n    setPrompt(\"\");\n    setAttachments([]);\n    setOpenMenu(null);\n  };\n\n  const context: PromptComposerContextValue = {\n    prompt,\n    setPrompt,\n    attachments,\n    addFiles: (files) => {\n      setAttachments((current) => [\n        ...current,\n        ...files.map((file) => {\n          attachmentId.current += 1;\n          return { id: String(attachmentId.current), file };\n        }),\n      ]);\n    },\n    removeAttachment: (id) => {\n      setAttachments((current) => current.filter((item) => item.id !== id));\n    },\n    busy,\n    invalid,\n    locked,\n    canSend,\n    expanded,\n    openMenu,\n    setOpenMenu,\n    inputId,\n    controlsRef,\n    addRef,\n    actionsRef,\n    inputRef,\n    focusInputOnReturnRef,\n    keepMenuOpenRef,\n    chrome,\n  };\n\n  return (\n    <PromptComposerContext.Provider value={context}>\n      <form\n        data-slot=\"prompt-composer\"\n        data-variant={variant}\n        data-invalid={invalid || undefined}\n        className={cn(\"relative\", disabled && \"opacity-55\", className)}\n        aria-busy={busy || undefined}\n        aria-disabled={disabled || undefined}\n        {...props}\n        onSubmit={submit}\n      >\n        <div\n          data-slot=\"prompt-composer-card\"\n          className={cn(\n            promptComposerCardVariants({\n              variant,\n              open: expanded || attachments.length > 0,\n              invalid,\n            }),\n          )}\n        >\n          <span\n            ref={measureRef}\n            aria-hidden=\"true\"\n            className={cn(\n              \"pointer-events-none invisible absolute whitespace-pre\",\n              chrome.fieldClass,\n            )}\n          >\n            {prompt}\n          </span>\n\n          {attachments.length > 0 ? (\n            <div className={cn(\"flex flex-wrap gap-1.5 pt-0.5\", chrome.pill ? \"px-1\" : \"px-0.5\")}>\n              {attachments.map((item) => (\n                <span\n                  key={item.id}\n                  className={cn(\n                    \"flex animate-in items-center gap-1.5 bg-muted py-1 pr-1 pl-1.5 text-muted-foreground duration-200 ease-out-quint fade-in-0 zoom-in-95 motion-reduce:animate-none\",\n                    chrome.chipClass,\n                  )}\n                >\n                  <FileText className=\"size-3\" aria-hidden=\"true\" />\n                  <span title={item.file.name} className=\"max-w-36 truncate text-card-foreground\">\n                    {item.file.name}\n                  </span>\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    aria-label={`Remove ${item.file.name}`}\n                    disabled={locked}\n                    onClick={() => context.removeAttachment(item.id)}\n                    className={cn(\n                      \"relative grid size-4 place-items-center text-muted-foreground transition-colors after:absolute after:-inset-1 after:content-[''] duration-100 hover:bg-transparent hover:text-card-foreground disabled:opacity-100 dark:hover:bg-transparent\",\n                      chrome.pill ? \"rounded-full\" : \"rounded\",\n                    )}\n                  >\n                    <X className=\"size-2.5\" strokeWidth={2.5} aria-hidden=\"true\" />\n                  </Button>\n                </span>\n              ))}\n            </div>\n          ) : null}\n\n          <div\n            ref={controlsRef}\n            className={cn(\n              \"grid items-end gap-1\",\n              expanded ? \"grid-cols-[auto_minmax(0,1fr)]\" : \"grid-cols-[auto_minmax(0,1fr)_auto]\",\n            )}\n          >\n            {children}\n          </div>\n        </div>\n      </form>\n    </PromptComposerContext.Provider>\n  );\n}\n\nexport type PromptComposerAddProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  children: ReactNode;\n  label?: string;\n};\n\nexport function PromptComposerAdd({\n  children,\n  label = \"Add attachments and sources\",\n  className,\n  ...props\n}: PromptComposerAddProps) {\n  const context = usePromptComposer(\"PromptComposerAdd\");\n  const open = context.openMenu === \"add\";\n\n  return (\n    <div\n      ref={context.addRef}\n      data-slot=\"prompt-composer-add\"\n      className={cn(\n        \"relative\",\n        context.expanded ? \"col-start-1 row-start-2\" : \"col-start-1 row-start-1\",\n        className,\n      )}\n      {...props}\n    >\n      <DropdownMenu modal={false} open={open} onOpenChange={menuOpenChange(context, \"add\")}>\n        <DropdownMenuTrigger asChild>\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon\"\n            aria-label={label}\n            disabled={context.locked}\n            onFocus={forwardReturnedFocus(context)}\n            className={cn(\n              \"text-muted-foreground transition-[background-color,color,scale] duration-150 focus-visible:bg-accent active:scale-[0.94] disabled:opacity-100 dark:hover:bg-accent aria-expanded:bg-accent aria-expanded:text-accent-foreground motion-reduce:transition-none motion-reduce:active:scale-100\",\n              context.chrome.controlClass,\n              context.chrome.controlRadiusClass,\n            )}\n          >\n            <span\n              className={cn(\n                \"grid transition-transform duration-160 ease-out-quint\",\n                open && \"rotate-45\",\n              )}\n            >\n              <Plus className={context.chrome.iconClass} strokeWidth={2} aria-hidden=\"true\" />\n            </span>\n          </Button>\n        </DropdownMenuTrigger>\n        <FloatingMenu kind=\"sources\">{children}</FloatingMenu>\n      </DropdownMenu>\n    </div>\n  );\n}\n\nexport type PromptComposerAddItemProps = Omit<\n  ComponentProps<typeof DropdownMenuItem>,\n  \"onSelect\"\n> & {\n  icon?: ReactNode;\n  description?: ReactNode;\n  onSelect?: () => void;\n};\n\nfunction PromptComposerMenuItem({\n  icon,\n  description,\n  children,\n  className,\n  disabled,\n  onSelect,\n  onClick,\n  ...props\n}: PromptComposerAddItemProps) {\n  const context = usePromptComposer(\"PromptComposerAddItem\");\n\n  return (\n    // `onClick` rather than Radix's `onSelect`: both menu primitives fire it for pointer, Enter,\n    // and Space, and Base UI has no `onSelect` item event.\n    <DropdownMenuItem\n      data-slot=\"prompt-composer-add-item\"\n      {...props}\n      onClick={(event) => {\n        onClick?.(event);\n        onSelect?.();\n      }}\n      disabled={context.locked || disabled}\n      className={cn(promptComposerMenuItemClass, className)}\n    >\n      {icon ? (\n        <span className=\"grid size-5 shrink-0 place-items-center text-muted-foreground transition-colors duration-120 group-data-highlighted/item:text-popover-foreground [&>svg]:size-4\">\n          {icon}\n        </span>\n      ) : null}\n      <span className=\"flex min-w-0 flex-1 items-baseline gap-2\">\n        <span className=\"shrink-0 truncate text-[13px]/[18px] font-medium text-popover-foreground\">\n          {children}\n        </span>\n        {description ? (\n          <span className=\"min-w-0 truncate text-[12.5px]/[18px] text-subtle-foreground\">\n            {description}\n          </span>\n        ) : null}\n      </span>\n    </DropdownMenuItem>\n  );\n}\n\nexport function PromptComposerAddItem(props: PromptComposerAddItemProps) {\n  return <PromptComposerMenuItem {...props} />;\n}\n\nexport type PromptComposerFileItemProps = Omit<\n  ComponentProps<\"input\">,\n  \"type\" | \"children\" | \"onChange\"\n> & {\n  label?: string;\n  description?: string;\n};\n\nexport function PromptComposerFileItem({\n  label = \"Add photos & files\",\n  description = \"Upload from your computer\",\n  multiple = true,\n  accept,\n  disabled,\n  className,\n  ...props\n}: PromptComposerFileItemProps) {\n  const context = usePromptComposer(\"PromptComposerFileItem\");\n  const inputRef = useRef<HTMLInputElement>(null);\n  const { setOpenMenu, keepMenuOpenRef } = context;\n\n  // The menu stays open while the file picker is up so the input remains mounted.\n  useEffect(() => {\n    const input = inputRef.current;\n    if (!input) return;\n    const close = () => {\n      keepMenuOpenRef.current = false;\n      setOpenMenu(null);\n    };\n    input.addEventListener(\"cancel\", close);\n    return () => input.removeEventListener(\"cancel\", close);\n  }, [setOpenMenu, keepMenuOpenRef]);\n\n  return (\n    <>\n      <PromptComposerMenuItem\n        icon={<Paperclip className=\"size-4\" strokeWidth={1.8} aria-hidden=\"true\" />}\n        description={description}\n        disabled={disabled}\n        onSelect={() => {\n          // Hold the menu open (it would close on click) until the picker reports back. If the\n          // browser never fires `cancel`, the next key or pointer press releases it.\n          keepMenuOpenRef.current = true;\n          expireOnNextInput(keepMenuOpenRef);\n          inputRef.current?.click();\n        }}\n      >\n        {label}\n      </PromptComposerMenuItem>\n      <input\n        ref={inputRef}\n        data-slot=\"prompt-composer-file-item\"\n        type=\"file\"\n        multiple={multiple}\n        accept={accept}\n        className={cn(\"hidden\", className)}\n        tabIndex={-1}\n        aria-label={label}\n        {...props}\n        disabled={context.locked || disabled}\n        onChange={(event) => {\n          const files = Array.from(event.target.files ?? []);\n          if (files.length) context.addFiles(files);\n          event.target.value = \"\";\n          keepMenuOpenRef.current = false;\n          setOpenMenu(null);\n        }}\n      />\n    </>\n  );\n}\n\nexport type PromptComposerInputProps = Omit<\n  ComponentProps<\"textarea\">,\n  \"value\" | \"defaultValue\" | \"onChange\"\n>;\n\nexport function PromptComposerInput({\n  placeholder = \"Write a message…\",\n  \"aria-label\": ariaLabel = \"Prompt\",\n  className,\n  onKeyDown,\n  disabled,\n  ...props\n}: PromptComposerInputProps) {\n  const context = usePromptComposer(\"PromptComposerInput\");\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n    onKeyDown?.(event);\n    if (event.defaultPrevented) return;\n    if (event.key === \"Escape\") {\n      context.setOpenMenu(null);\n      return;\n    }\n    if (event.key === \"Enter\" && !event.shiftKey && !event.nativeEvent.isComposing) {\n      event.preventDefault();\n      event.currentTarget.form?.requestSubmit();\n    }\n  };\n\n  return (\n    <Textarea\n      data-slot=\"prompt-composer-input\"\n      {...props}\n      ref={context.inputRef}\n      id={context.inputId}\n      rows={1}\n      value={context.prompt}\n      disabled={context.locked || disabled}\n      aria-label={ariaLabel}\n      aria-invalid={context.invalid || undefined}\n      placeholder={placeholder}\n      onChange={(event) => {\n        context.setPrompt(event.target.value);\n        context.setOpenMenu(null);\n      }}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"resize-none overflow-y-auto rounded-none border-0 bg-transparent px-1 text-card-foreground caret-card-foreground shadow-none outline-none! field-sizing-content selection:bg-foreground/18 placeholder:text-subtle-foreground focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-100 dark:bg-transparent\",\n        context.chrome.fieldClass,\n        context.chrome.maxFieldHeightClass,\n        context.expanded\n          ? \"col-span-2 col-start-1 row-start-1 w-full\"\n          : \"col-start-2 row-start-1 min-w-0 w-full\",\n        className,\n      )}\n    />\n  );\n}\n\nexport type PromptComposerActionsProps = ComponentProps<\"div\">;\n\nexport function PromptComposerActions({\n  className,\n  children,\n  ...props\n}: PromptComposerActionsProps) {\n  const context = usePromptComposer(\"PromptComposerActions\");\n\n  return (\n    <div\n      ref={context.actionsRef}\n      data-slot=\"prompt-composer-actions\"\n      className={cn(\n        \"flex items-center gap-1\",\n        context.expanded ? \"col-start-2 row-start-2 justify-self-end\" : \"col-start-3 row-start-1\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type PromptComposerModelSelectProps = Omit<ComponentProps<\"div\">, \"onChange\"> & {\n  models: readonly PromptComposerModel[];\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (modelId: string) => void;\n  label?: string;\n};\n\nexport function PromptComposerModelSelect({\n  models,\n  value,\n  defaultValue,\n  onValueChange,\n  label = \"Choose model\",\n  className,\n  ...props\n}: PromptComposerModelSelectProps) {\n  const context = usePromptComposer(\"PromptComposerModelSelect\");\n  const [internalValue, setInternalValue] = useState(defaultValue ?? models[0]?.id ?? \"\");\n  const selectedId = value ?? internalValue;\n  const selected = models.find((model) => model.id === selectedId) ?? models[0];\n  const open = context.openMenu === \"model\";\n\n  if (!selected) return null;\n\n  return (\n    <div data-slot=\"prompt-composer-model-select\" className={cn(\"relative\", className)} {...props}>\n      {models.length > 1 ? (\n        <DropdownMenu modal={false} open={open} onOpenChange={menuOpenChange(context, \"model\")}>\n          <DropdownMenuTrigger asChild>\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"sm\"\n              aria-label={`${label}: ${selected.label}`}\n              disabled={context.locked}\n              onFocus={forwardReturnedFocus(context)}\n              className={cn(\n                \"gap-1 px-2 text-muted-foreground transition-colors duration-120 ease-out focus-visible:bg-accent disabled:opacity-100 has-[>svg]:px-2 motion-reduce:transition-none dark:hover:bg-accent aria-expanded:bg-accent aria-expanded:text-accent-foreground\",\n                context.chrome.controlHeightClass,\n                context.chrome.controlRadiusClass,\n                context.chrome.modelClass,\n              )}\n            >\n              {selected.label}\n              <ChevronDown\n                className={cn(\n                  \"size-3 transition-transform duration-180 ease-out-quint motion-reduce:transition-none\",\n                  open && \"rotate-180\",\n                )}\n                strokeWidth={2.4}\n                aria-hidden=\"true\"\n              />\n            </Button>\n          </DropdownMenuTrigger>\n          <FloatingMenu kind=\"models\">\n            <DropdownMenuRadioGroup\n              value={selected.id}\n              onValueChange={(modelId) => {\n                if (value === undefined) setInternalValue(modelId);\n                onValueChange?.(modelId);\n              }}\n            >\n              {models.map((model) => (\n                <DropdownMenuRadioItem\n                  key={model.id}\n                  value={model.id}\n                  className={cn(\n                    promptComposerMenuItemClass,\n                    \"min-h-8 py-0 pl-2 [&>span:first-child]:hidden\",\n                  )}\n                >\n                  <span className=\"min-w-0 flex-1 truncate text-[12.5px] font-medium text-popover-foreground\">\n                    {model.label}\n                  </span>\n                  <Check\n                    className={cn(\n                      \"size-3\",\n                      model.id === selected.id ? \"text-popover-foreground\" : \"invisible\",\n                    )}\n                    strokeWidth={2.5}\n                    aria-hidden=\"true\"\n                  />\n                </DropdownMenuRadioItem>\n              ))}\n            </DropdownMenuRadioGroup>\n          </FloatingMenu>\n        </DropdownMenu>\n      ) : (\n        <span\n          className={cn(\n            \"flex shrink-0 items-center px-1.5 font-medium text-muted-foreground\",\n            context.chrome.controlHeightClass,\n            context.chrome.modelClass,\n          )}\n        >\n          {selected.label}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport type PromptComposerSubmitProps = ComponentProps<\"button\">;\n\nexport function PromptComposerSubmit({\n  \"aria-label\": ariaLabel,\n  className,\n  children,\n  disabled,\n  ...props\n}: PromptComposerSubmitProps) {\n  const context = usePromptComposer(\"PromptComposerSubmit\");\n\n  return (\n    <Button\n      data-slot=\"prompt-composer-submit\"\n      size=\"icon\"\n      {...props}\n      type=\"submit\"\n      aria-label={ariaLabel ?? (context.busy ? \"Sending prompt\" : \"Send\")}\n      disabled={!context.canSend || disabled}\n      className={cn(\n        \"transition-[background-color,color,opacity,scale] duration-140 ease-out-quint enabled:hover:opacity-90 enabled:active:scale-[0.94] disabled:cursor-not-allowed disabled:opacity-100 motion-reduce:transition-none motion-reduce:enabled:active:scale-100\",\n        context.chrome.controlClass,\n        context.chrome.controlRadiusClass,\n        context.canSend || context.busy\n          ? \"bg-foreground text-card hover:bg-foreground\"\n          : \"bg-border-strong text-muted-foreground hover:bg-border-strong\",\n        className,\n      )}\n    >\n      {children ?? (\n        <span className=\"grid [&>*]:[grid-area:1/1]\">\n          <span\n            className={cn(\n              \"grid place-items-center transition-[opacity,scale,filter] duration-200 ease-out-quint motion-reduce:transition-none\",\n              context.busy ? \"scale-100 blur-none opacity-100\" : \"scale-50 opacity-0 blur-[2px]\",\n            )}\n          >\n            <LoaderCircle\n              className={cn(context.chrome.iconClass, \"motion-safe:animate-spin\")}\n              aria-hidden=\"true\"\n            />\n          </span>\n          <span\n            className={cn(\n              \"grid place-items-center transition-[opacity,scale,filter] duration-200 ease-out-quint motion-reduce:transition-none\",\n              context.busy ? \"scale-50 opacity-0 blur-[2px]\" : \"scale-100 blur-none opacity-100\",\n            )}\n          >\n            <ArrowUp className={context.chrome.iconClass} strokeWidth={2.4} aria-hidden=\"true\" />\n          </span>\n        </span>\n      )}\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/prompt-composer.tsx"
    }
  ],
  "categories": [
    "ai",
    "forms"
  ],
  "type": "registry:ui"
}