"use client"; import { useCallback, useEffect, useId, useRef, useState, type KeyboardEvent, } from "react"; import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { Check, ChevronDown, Flower } from "lucide-react"; import { useReducedMotion } from "motion/react"; import { promptComposerSpring } from "./prompt-composer-motion"; gsap.registerPlugin(useGSAP); export interface PromptComposerModel { id: string; name: string; description?: string; } export function PromptComposerModelPicker({ models, value, onChange, disabled, speed, stiffness, damping, }: { models: readonly PromptComposerModel[]; value: PromptComposerModel; onChange: (model: PromptComposerModel) => void; disabled: boolean; speed: number; stiffness: number; damping: number; }) { const [open, setOpen] = useState(false); const rootRef = useRef(null); const triggerRef = useRef(null); const menuRef = useRef(null); const highlightRef = useRef(null); const pinnedRef = useRef(false); const focusOnOpenRef = useRef(false); const focusEdgeRef = useRef<"first" | "last" | "selected">("selected"); const closeDelayRef = useRef(null); const highlightedRef = useRef(false); const [hovered, setHovered] = useState(null); const menuId = useId(); const reduced = useReducedMotion(); const visible = open && !disabled; const { contextSafe } = useGSAP(() => () => closeDelayRef.current?.kill(), { scope: rootRef, }); const close = useCallback(() => { closeDelayRef.current?.kill(); focusOnOpenRef.current = false; pinnedRef.current = false; setOpen(false); setHovered(null); }, []); const focusMenu = useCallback((edge: "first" | "last" | "selected") => { const options = Array.from( menuRef.current?.querySelectorAll( '[role="menuitemradio"]', ) ?? [], ); const option = edge === "first" ? options[0] : edge === "last" ? options.at(-1) : options.find( (item) => item.getAttribute("aria-checked") === "true", ); option?.focus({ preventScroll: true }); }, []); const scheduleClose = () => contextSafe(() => { if ( pinnedRef.current || menuRef.current?.contains(document.activeElement) ) return; closeDelayRef.current?.kill(); closeDelayRef.current = gsap.delayedCall(0.18, close); })(); useGSAP( () => { gsap.to(menuRef.current, { autoAlpha: visible ? 1 : 0, y: visible ? 0 : 7, scale: visible ? 1 : 0.96, ...promptComposerSpring(stiffness, damping, speed, reduced), overwrite: true, }); gsap.to("[data-model-chevron]", { rotation: visible ? 180 : 0, ...promptComposerSpring(stiffness, damping, speed, reduced), overwrite: true, }); }, { scope: rootRef, dependencies: [visible, reduced, speed, stiffness, damping], }, ); useGSAP( () => { const target = Array.from( menuRef.current?.querySelectorAll( "[data-model-option]", ) ?? [], ).find((option) => option.dataset.modelOption === (hovered ?? value.id)); if (!target || !visible) { highlightedRef.current = false; return; } const vars = { y: target.offsetTop, height: target.offsetHeight, autoAlpha: 1, }; if (!highlightedRef.current || reduced) gsap.set(highlightRef.current, vars); else gsap.to(highlightRef.current, { ...vars, ...promptComposerSpring(stiffness, damping, speed, reduced), overwrite: true, }); highlightedRef.current = true; }, { scope: rootRef, dependencies: [ hovered, visible, value.id, reduced, speed, stiffness, damping, models.map((model) => model.id).join(","), ], }, ); useEffect(() => { if (!visible) return; if (focusOnOpenRef.current) focusMenu(focusEdgeRef.current); focusOnOpenRef.current = false; const dismiss = (event: PointerEvent) => { if ( event.target instanceof Node && !rootRef.current?.contains(event.target) ) close(); }; document.addEventListener("pointerdown", dismiss); return () => document.removeEventListener("pointerdown", dismiss); }, [visible, close, focusMenu]); const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); triggerRef.current?.focus({ preventScroll: true }); return; } const options = Array.from( menuRef.current?.querySelectorAll( '[role="menuitemradio"]', ) ?? [], ); const index = options.indexOf(document.activeElement as HTMLButtonElement); const next = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 : event.key === "ArrowDown" ? (index + 1) % options.length : event.key === "ArrowUp" ? (index - 1 + options.length) % options.length : null; if (next !== null) { event.preventDefault(); options[next]?.focus({ preventScroll: true }); } }; return (
{ closeDelayRef.current?.kill(); if (event.pointerType === "mouse" && !disabled && models.length > 1) setOpen(true); }} onPointerLeave={(event) => { if (event.pointerType === "mouse") scheduleClose(); }} onBlur={(event) => { if (!event.currentTarget.contains(event.relatedTarget)) close(); }} >
); }