"use client"; import * as React from "react"; import { useRef, useState, useEffect, useLayoutEffect, useCallback, useId, useSyncExternalStore, } from "react"; import { createPortal } from "react-dom"; import { cn } from "@/lib/utils"; const SPRING_TRANSITION = "max-width 0.42s cubic-bezier(0.16, 1, 0.3, 1), height 0.42s cubic-bezier(0.16, 1, 0.3, 1), border-radius 0.28s ease, box-shadow 0.3s ease"; const SMOOTH_HEIGHT_TRANSITION = "max-width 0.42s cubic-bezier(0.16, 1, 0.3, 1), height 0.16s cubic-bezier(0.2, 0, 0, 1), box-shadow 0.3s ease"; const emptySubscribe = () => () => {}; export interface Attachment { id: string; file: File; url: string; name: string; width?: number; height?: number; } export interface ModelOption { name: string; icon?: string | React.ReactNode; } export interface PromptInputMeta { model: string; effort: string; attachments: File[]; } export interface PromptInputRef { focus: () => void; expand: () => void; collapse: () => void; startVoice: () => void; stopVoice: () => void; addAttachment: ( file: File, url: string, width?: number, height?: number, ) => void; clear: () => void; element: HTMLDivElement | null; } export interface PromptInputProps { onSubmit?: (value: string, meta: PromptInputMeta) => void; placeholder?: string; className?: string; models?: (string | ModelOption)[]; selectedModel?: string; onModelChange?: (model: string) => void; efforts?: string[]; selectedEffort?: string; onEffortChange?: (effort: string) => void; defaultValue?: string; value?: string; onChange?: (value: string) => void; expanded?: boolean; onExpandedChange?: (expanded: boolean) => void; maxAttachments?: number; allowAttachments?: boolean; allowVoice?: boolean; allowModelSelect?: boolean; allowEffortSelect?: boolean; minWidth?: number | string; maxWidth?: number | string; collapsedHeight?: number; autoFocus?: boolean; disabled?: boolean; accentColor?: string; variant?: "default" | "glow" | "minimal"; modelIcons?: Record; } interface BrowserSpeechRecognitionEvent { resultIndex: number; results: { length: number; [index: number]: { isFinal: boolean; [index: number]: { transcript: string }; }; }; } interface BrowserSpeechRecognition { continuous: boolean; interimResults: boolean; start: () => void; stop: () => void; onresult: ((event: BrowserSpeechRecognitionEvent) => void) | null; onerror: (() => void) | null; onend: (() => void) | null; } type BrowserSpeechRecognitionConstructor = new () => BrowserSpeechRecognition; const DEFAULT_MODELS: string[] = [ "GPT 5.5", "Opus 4.8", "Gemini 3.5 Flash", "Composer 2.5", "GLM 5.2", ]; const DEFAULT_EFFORTS: string[] = ["Low", "Medium", "Max Effort"]; const DEFAULT_ICONS: Record = { "Composer 2.5": "https://cdn.21st.dev/assets/mirror/7d/7dc00bc09f225fcda46cbc9c6b669c69c025a231877d6c17baa6a003f04f02b2.svg", "Gemini 3.5 Flash": "https://cdn.21st.dev/assets/mirror/cd/cda2df6631d5fa227de3fa04ed78cf354f910ba92a9f086e7455655c10ad9d09.svg", "GPT 5.5": "https://cdn.21st.dev/assets/mirror/b9/b93fa7942be639a1dae60194ff12141145d7d9fd59581582d6ff23335755f19c.svg", "Opus 4.8": "https://cdn.21st.dev/assets/mirror/5d/5de1221c77cc91e748066fd642ad0eee1c1fa65328814f5178166f901e599709.svg", "GLM 5.2": "https://cdn.21st.dev/assets/mirror/b2/b2a6c0ff63efd8a555edf8a174ea6fcfeca120ac1595a2d461ca11d3ae89276c.svg", }; function ArrowUpIcon() { return ( ); } function MicIcon() { return ( ); } function StopIcon() { return ( ); } function PlusIcon() { return ( ); } function CloseIcon() { return ( ); } function CheckIcon() { return ( ); } function DynamicBarsIcon({ level }: { level: string }) { const isMediumOrHigh = level === "Medium" || level === "Max Effort"; const isHigh = level === "Max Effort"; return ( ); } function ModelFallbackIcon({ className }: { className?: string }) { return ( ); } function ModelIcon({ model, customIcon, className, }: { model: string; customIcon?: string | React.ReactNode; className?: string; }) { const [loadFailed, setLoadFailed] = useState(false); if (customIcon && typeof customIcon !== "string") { return ( {customIcon} ); } const iconSrc = typeof customIcon === "string" ? customIcon : DEFAULT_ICONS[model]; if (!iconSrc || loadFailed) { return ( ); } return ( {model} setLoadFailed(true)} className={cn( "size-3.5 shrink-0 object-contain", model === "GPT 5.5" && "dark:invert", className, )} /> ); } function MorphingText({ text }: { text: string }) { const spanRef = useRef(null); const [width, setWidth] = useState("auto"); useLayoutEffect(() => { if (spanRef.current) { setWidth(spanRef.current.getBoundingClientRect().width); } }, [text]); return ( {text} {text} ); } function AttachmentThumb({ attachment, index, onRemove, onOpen, registerRef, }: { attachment: Attachment; index: number; onRemove: (id: string) => void; onOpen: (attachment: Attachment, rect: DOMRect) => void; registerRef: (id: string, el: HTMLButtonElement | null) => void; }) { const [isHovered, setIsHovered] = useState(false); const btnRef = useRef(null); return ( ); } function AttachmentGalleryModal({ attachment, originRect, onClose, }: { attachment: Attachment; originRect: DOMRect; onClose: () => void; }) { const [phase, setPhase] = useState<"opening" | "open" | "closing">("opening"); const targetGeometry = React.useMemo(() => { if (typeof window === "undefined") { return { top: 0, left: 0, width: 300, height: 200, radius: 20 }; } const maxW = Math.min(window.innerWidth * 0.88, 620); const maxH = Math.min(window.innerHeight * 0.82, 760); const naturalW = attachment.width || 800; const naturalH = attachment.height || 600; const scale = Math.min(maxW / naturalW, maxH / naturalH, 1.5); const width = naturalW * scale; const height = naturalH * scale; return { top: (window.innerHeight - height) / 2, left: (window.innerWidth - width) / 2, width, height, radius: 20, }; }, [attachment.width, attachment.height]); useEffect(() => { const raf = requestAnimationFrame(() => setPhase("open")); return () => cancelAnimationFrame(raf); }, []); const handleClose = useCallback(() => setPhase("closing"), []); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") handleClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [handleClose]); const isOpen = phase === "open"; const isClosing = phase === "closing"; const geometry = isOpen ? targetGeometry : { top: originRect.top, left: originRect.left, width: originRect.width, height: originRect.height, radius: 12, }; const animEasing = isClosing ? "ease-out" : "cubic-bezier(0.16, 1, 0.3, 1)"; const animDur = isClosing ? "0.26s" : "0.38s"; const flipTransition = `top ${animDur} ${animEasing}, left ${animDur} ${animEasing}, width ${animDur} ${animEasing}, height ${animDur} ${animEasing}, border-radius ${animDur} ${animEasing}`; return createPortal(
{ if (phase === "closing") onClose(); }} onClick={(e) => e.stopPropagation()} > {attachment.name}
, document.body, ); } export const PromptInput = React.forwardRef( ( { onSubmit, placeholder = "Ask anything", className, models = DEFAULT_MODELS, selectedModel: controlledModel, onModelChange, efforts = DEFAULT_EFFORTS, selectedEffort: controlledEffort, onEffortChange, defaultValue = "", value: controlledValue, onChange, expanded: controlledExpanded, onExpandedChange, maxAttachments = 6, allowAttachments = true, allowVoice = true, allowModelSelect = true, allowEffortSelect = true, minWidth = 320, maxWidth = 480, collapsedHeight = 48, autoFocus = false, disabled = false, accentColor, variant = "default", modelIcons, }, ref, ) => { const inputId = useId(); const mounted = useSyncExternalStore( emptySubscribe, () => true, () => false, ); const normalizedModels: ModelOption[] = React.useMemo(() => { return models.map((m) => { if (typeof m === "string") { return { name: m, icon: modelIcons?.[m] }; } return { name: m.name, icon: m.icon ?? modelIcons?.[m.name] }; }); }, [models, modelIcons]); const isControlledModel = controlledModel !== undefined; const [internalModel, setInternalModel] = useState( controlledModel || (normalizedModels[0]?.name ?? "GPT 5.5"), ); const activeModel = isControlledModel ? controlledModel : internalModel; const isControlledEffort = controlledEffort !== undefined; const [internalEffortIndex, setInternalEffortIndex] = useState(1); const currentEffort = isControlledEffort ? controlledEffort : efforts[internalEffortIndex] || efforts[0] || "Medium"; const [isSmoothResize, setIsSmoothResize] = useState(false); const [localValue, setLocalValue] = useState(defaultValue); const [isModelSelectOpen, setIsModelSelectOpen] = useState(false); const [attachments, setAttachments] = useState([]); const [activeAttachment, setActiveAttachment] = useState<{ attachment: Attachment; rect: DOMRect; } | null>(null); const [isRecording, setIsRecording] = useState(false); const [audioData, setAudioData] = useState(new Array(5).fill(0)); const valueRef = useRef( controlledValue !== undefined ? controlledValue : localValue, ); const streamRef = useRef(null); const audioContextRef = useRef(null); const rafRef = useRef(null); const recognitionRef = useRef(null); const demoIntervalRef = useRef(null); const demoTextIntervalRef = useRef(null); const initialModelIdx = Math.max( 0, normalizedModels.findIndex((m) => m.name === activeModel), ); const [hoverStyle, setHoverStyle] = useState({ opacity: 0, transform: `translateY(${initialModelIdx * 34}px) scale(0.96)`, transition: "none", }); const [textareaHeight, setTextareaHeight] = useState(68); const containerHeight = Math.max(116, textareaHeight + 48); const [isScrolling, setIsScrolling] = useState(false); const isControlled = controlledValue !== undefined; const value = isControlled ? controlledValue : localValue; const hasAttachments = attachments.length > 0; const hasValue = value.trim() !== "" || hasAttachments; const isControlledExpanded = controlledExpanded !== undefined; const [internalExpanded, setInternalExpanded] = useState( Boolean( autoFocus || (defaultValue && defaultValue.trim() !== "") || (controlledValue && controlledValue.trim() !== ""), ), ); const expanded = isControlledExpanded ? controlledExpanded : internalExpanded || hasValue; const textareaRef = useRef(null); const internalContainerRef = useRef(null); const topFadeRef = useRef(null); const bottomFadeRef = useRef(null); const fileInputRef = useRef(null); const modelTriggerRef = useRef(null); const modelMenuRef = useRef(null); const modelMenuId = `${inputId}-models`; const thumbRefs = useRef>(new Map()); useEffect(() => { valueRef.current = value; }, [value]); const updateFades = useCallback(() => { const el = textareaRef.current; if (!el) return; const { scrollTop, scrollHeight, clientHeight } = el; if (topFadeRef.current) { topFadeRef.current.style.opacity = Math.min( scrollTop / 20, 1, ).toString(); } if (bottomFadeRef.current) { const bottomScroll = scrollHeight - clientHeight - scrollTop; bottomFadeRef.current.style.opacity = Math.min( Math.max(bottomScroll - 16, 0) / 10, 1, ).toString(); } }, []); const handleValueChange = useCallback( (val: string) => { setIsSmoothResize(true); if (!isControlled) setLocalValue(val); onChange?.(val); if (val.trim() !== "" && !internalExpanded) { setInternalExpanded(true); onExpandedChange?.(true); } }, [isControlled, onChange, internalExpanded, onExpandedChange], ); const expand = useCallback(() => { setIsSmoothResize(false); setInternalExpanded(true); onExpandedChange?.(true); }, [onExpandedChange]); const stopRecording = useCallback(() => { if (recognitionRef.current) { try { recognitionRef.current.stop(); } catch {} recognitionRef.current = null; } if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } if (streamRef.current) { streamRef.current.getTracks().forEach((track) => track.stop()); streamRef.current = null; } if (audioContextRef.current) { try { audioContextRef.current.close(); } catch {} audioContextRef.current = null; } if (demoIntervalRef.current) { window.clearInterval(demoIntervalRef.current); demoIntervalRef.current = null; } if (demoTextIntervalRef.current) { window.clearInterval(demoTextIntervalRef.current); demoTextIntervalRef.current = null; } setIsRecording(false); setAudioData(new Array(5).fill(0)); }, []); const startRecording = useCallback(async () => { setIsSmoothResize(false); setInternalExpanded(true); onExpandedChange?.(true); let stream: MediaStream | null = null; try { if ( typeof navigator !== "undefined" && navigator.mediaDevices?.getUserMedia ) { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); } } catch { stream = null; } setIsRecording(true); const simulateText = () => { const fakeText = "Build a high performance, accessible AI prompt input with smooth spring transitions."; const words = fakeText.split(" "); let i = 0; let currentBase = valueRef.current; demoTextIntervalRef.current = window.setInterval(() => { if (i < words.length) { currentBase = (currentBase ? currentBase + " " : "") + words[i]; handleValueChange(currentBase); i++; } else { stopRecording(); } }, 260); }; if (stream) { streamRef.current = stream; const AudioCtx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }) .webkitAudioContext; const audioCtx = new AudioCtx(); audioContextRef.current = audioCtx; const analyser = audioCtx.createAnalyser(); analyser.fftSize = 64; const source = audioCtx.createMediaStreamSource(stream); source.connect(analyser); const dataArray = new Uint8Array(analyser.frequencyBinCount); const updateVisualizer = () => { analyser.getByteFrequencyData(dataArray); const bands = new Array(5).fill(0); const step = Math.floor(dataArray.length / 5); for (let i = 0; i < 5; i++) { let sum = 0; for (let j = 0; j < step; j++) { sum += dataArray[i * step + j]; } bands[i] = sum / step / 255; } setAudioData(bands); rafRef.current = requestAnimationFrame(updateVisualizer); }; updateVisualizer(); const SpeechRec = ( window as unknown as { SpeechRecognition?: BrowserSpeechRecognitionConstructor; webkitSpeechRecognition?: BrowserSpeechRecognitionConstructor; } ).SpeechRecognition || ( window as unknown as { SpeechRecognition?: BrowserSpeechRecognitionConstructor; webkitSpeechRecognition?: BrowserSpeechRecognitionConstructor; } ).webkitSpeechRecognition; if (SpeechRec) { const recognition = new SpeechRec(); recognition.continuous = true; recognition.interimResults = true; let baseline = valueRef.current; recognition.onresult = (event: BrowserSpeechRecognitionEvent) => { let interimTranscript = ""; let finalTranscript = ""; for (let i = event.resultIndex; i < event.results.length; ++i) { if (event.results[i].isFinal) { finalTranscript += event.results[i][0].transcript; } else { interimTranscript += event.results[i][0].transcript; } } if (finalTranscript) { baseline += (baseline ? " " : "") + finalTranscript; } handleValueChange( ( baseline + (interimTranscript ? " " + interimTranscript : "") ).trim(), ); }; recognition.onerror = () => { stopRecording(); }; recognition.onend = () => { stopRecording(); }; recognitionRef.current = recognition; try { recognition.start(); } catch { simulateText(); } } else { simulateText(); } } else { demoIntervalRef.current = window.setInterval(() => { setAudioData( Array.from({ length: 5 }, () => Math.random() * 0.75 + 0.15), ); }, 90); simulateText(); } }, [handleValueChange, stopRecording, onExpandedChange]); useEffect(() => { if (isRecording && textareaRef.current) { textareaRef.current.scrollTop = textareaRef.current.scrollHeight; } }, [value, isRecording]); useEffect(() => { return () => { stopRecording(); attachments.forEach((a) => URL.revokeObjectURL(a.url)); }; }, [stopRecording, attachments]); useEffect(() => { if (expanded && !isRecording) { const timer = setTimeout(() => { if (textareaRef.current) { textareaRef.current.focus(); const length = textareaRef.current.value.length; textareaRef.current.setSelectionRange(length, length); } }, 40); return () => clearTimeout(timer); } }, [expanded, isRecording]); useEffect(() => { const el = textareaRef.current; if (!el) return; el.style.height = "auto"; const scrollHeight = el.scrollHeight; const newHeight = Math.max(68, Math.min(scrollHeight, 180)); el.style.height = `${newHeight}px`; setTextareaHeight(newHeight); setIsScrolling(scrollHeight > 180); updateFades(); }, [value, expanded, updateFades]); const closeModelSelect = useCallback(() => { const restoreFocus = modelMenuRef.current?.contains( document.activeElement, ); setIsModelSelectOpen(false); if (restoreFocus) modelTriggerRef.current?.focus(); }, []); useEffect(() => { if (!isModelSelectOpen) return; const handleOutsideClick = (e: MouseEvent) => { if ( internalContainerRef.current && !internalContainerRef.current.contains(e.target as Node) ) { setIsModelSelectOpen(false); } }; const handleEscape = (e: KeyboardEvent) => { if (e.key !== "Escape") return; e.preventDefault(); e.stopPropagation(); closeModelSelect(); }; document.addEventListener("mousedown", handleOutsideClick); window.addEventListener("keydown", handleEscape, true); return () => { document.removeEventListener("mousedown", handleOutsideClick); window.removeEventListener("keydown", handleEscape, true); }; }, [isModelSelectOpen, closeModelSelect]); const handleBlur = (e: React.FocusEvent) => { if ( internalContainerRef.current && internalContainerRef.current.contains(e.relatedTarget as Node) ) { return; } if (value.trim() === "" && !hasAttachments && !isRecording) { setIsSmoothResize(false); setInternalExpanded(false); onExpandedChange?.(false); setIsModelSelectOpen(false); } }; const handleSubmit = () => { if (value.trim() === "" && !hasAttachments) return; setIsSmoothResize(false); onSubmit?.(value, { model: activeModel, effort: currentEffort, attachments: attachments.map((a) => a.file), }); handleValueChange(""); attachments.forEach((a) => URL.revokeObjectURL(a.url)); setAttachments([]); setInternalExpanded(false); onExpandedChange?.(false); setIsModelSelectOpen(false); }; const cycleEffort = (e: React.MouseEvent) => { e.stopPropagation(); const nextIdx = (internalEffortIndex + 1) % efforts.length; if (!isControlledEffort) { setInternalEffortIndex(nextIdx); } onEffortChange?.(efforts[nextIdx]); }; const selectModel = (modelName: string) => { if (!isControlledModel) { setInternalModel(modelName); } onModelChange?.(modelName); closeModelSelect(); }; const activeModelIndex = Math.max( 0, normalizedModels.findIndex((m) => m.name === activeModel), ); const highlightModel = (idx: number) => { setHoverStyle({ opacity: 1, transform: `translateY(${idx * 34}px) scale(1)`, transition: "transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.15s ease", }); }; const focusModelOption = (idx: number) => { const options = modelMenuRef.current?.querySelectorAll( '[role="menuitemradio"]', ); if (!options?.length) return; options[(idx + options.length) % options.length].focus(); }; const openModelSelectWithFocus = () => { setIsModelSelectOpen(true); requestAnimationFrame(() => focusModelOption(activeModelIndex)); }; const handleModelMenuKeyDown = (e: React.KeyboardEvent) => { const options = Array.from( e.currentTarget.querySelectorAll('[role="menuitemradio"]'), ); const current = options.indexOf(document.activeElement as HTMLElement); if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); focusModelOption(current + (e.key === "ArrowDown" ? 1 : -1)); } else if (e.key === "Home" || e.key === "End") { e.preventDefault(); focusModelOption(e.key === "Home" ? 0 : options.length - 1); } else if (e.key === "Tab") { setIsModelSelectOpen(false); } }; const openFileChooser = (e: React.MouseEvent) => { e.stopPropagation(); fileInputRef.current?.click(); }; const addAttachment = useCallback( (file: File, url: string, width = 800, height = 600) => { const id = `${file.name}-${file.lastModified || Date.now()}-${Math.random().toString(36).slice(2, 8)}`; setAttachments((prev) => [ ...prev, { id, file, url, name: file.name, width, height }, ]); setInternalExpanded(true); onExpandedChange?.(true); }, [onExpandedChange], ); const handleFilesChosen = async ( e: React.ChangeEvent, ) => { const files = Array.from(e.target.files ?? []).filter((f) => f.type.startsWith("image/"), ); e.target.value = ""; if (files.length === 0) return; const room = Math.max(0, maxAttachments - attachments.length); const accepted = files.slice(0, room); if (!expanded) { setIsSmoothResize(false); setInternalExpanded(true); onExpandedChange?.(true); } else { setIsSmoothResize(true); } for (const file of accepted) { const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => addAttachment(file, url, img.naturalWidth, img.naturalHeight); img.onerror = () => addAttachment(file, url, 800, 600); img.src = url; } }; React.useImperativeHandle( ref, () => ({ focus: () => textareaRef.current?.focus(), expand: () => { setIsSmoothResize(false); setInternalExpanded(true); onExpandedChange?.(true); }, collapse: () => { setIsSmoothResize(false); setInternalExpanded(false); onExpandedChange?.(false); }, startVoice: () => { startRecording(); }, stopVoice: () => { stopRecording(); }, addAttachment: (file: File, url: string, width = 800, height = 600) => { addAttachment(file, url, width, height); }, clear: () => { handleValueChange(""); attachments.forEach((a) => URL.revokeObjectURL(a.url)); setAttachments([]); setInternalExpanded(false); onExpandedChange?.(false); }, element: internalContainerRef.current, }), [ startRecording, stopRecording, addAttachment, handleValueChange, attachments, onExpandedChange, ], ); const removeAttachment = (id: string) => { setIsSmoothResize(true); setAttachments((prev) => { const target = prev.find((a) => a.id === id); if (target) URL.revokeObjectURL(target.url); return prev.filter((a) => a.id !== id); }); thumbRefs.current.delete(id); }; const showArrow = hasValue && !isRecording; const showStop = isRecording; const showMic = !hasValue && !isRecording; const onActionButtonClick = (e: React.MouseEvent) => { e.preventDefault(); if (disabled) return; if (isRecording) { stopRecording(); } else if (hasValue) { handleSubmit(); } else if (allowVoice) { startRecording(); } }; const resolvedMinW = typeof minWidth === "number" ? `${minWidth}px` : minWidth; const resolvedMaxW = typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth; const customGlowShadow = variant === "glow" ? accentColor ? `0 0 35px -5px ${accentColor}33, 0 16px 40px rgba(0, 0, 0, 0.65)` : "0 0 35px -5px rgba(255, 255, 255, 0.12), 0 16px 40px rgba(0, 0, 0, 0.65)" : undefined; return ( <>
{ ( internalContainerRef as React.MutableRefObject ).current = node; }} onBlur={handleBlur} className={cn( "relative flex flex-col w-full items-center", className, )} style={{ maxWidth: expanded ? `min(100%, ${resolvedMaxW})` : `min(100%, ${resolvedMinW})`, transition: isSmoothResize ? "max-width 0.16s ease-out" : "max-width 0.42s cubic-bezier(0.16, 1, 0.3, 1)", }} > {allowAttachments && ( )}
{attachments.map((attachment, index) => ( setActiveAttachment({ attachment: a, rect }) } registerRef={(id, el) => thumbRefs.current.set(id, el)} /> ))}
{ const isTextarea = e.target === textareaRef.current; if (expanded && !isTextarea && !isRecording && !disabled) { e.preventDefault(); textareaRef.current?.focus(); } }} style={{ borderRadius: 24, height: expanded ? containerHeight : collapsedHeight, transition: isSmoothResize ? SMOOTH_HEIGHT_TRANSITION : SPRING_TRANSITION, overflow: expanded ? "visible" : "hidden", boxShadow: customGlowShadow, }} className={cn( "relative w-full border bg-white/95 dark:bg-[#0c0c0e]/95 backdrop-blur-xl shadow-[0_4px_20px_rgba(0,0,0,0.08)] dark:shadow-[0_16px_40px_rgba(0,0,0,0.6)] z-10 transition-colors duration-200", variant === "glow" ? "border-zinc-300 dark:border-white/20" : variant === "minimal" ? "border-zinc-200 dark:border-white/8 dark:bg-[#09090b]/90" : "border-zinc-200 dark:border-white/12", "focus-within:border-zinc-400 focus-within:ring-1 focus-within:ring-zinc-300 dark:focus-within:border-white/30 dark:focus-within:ring-white/20 hover:border-zinc-300 dark:hover:border-white/20", expanded ? "cursor-text" : "cursor-default", disabled && "opacity-60 pointer-events-none", )} >