"use client"; import { useState, useRef, useEffect, useId, useMemo, useCallback, type ChangeEvent } from "react"; import { motion } from "motion/react"; import { cn } from "@/lib/utils"; function GooeyFilter({ filterId, blur }: { filterId: string; blur: number }) { return ( ); } function SearchIcon({ layoutId }: { layoutId: string }) { return ( ); } const transition = { duration: 0.4, type: "spring" as const, bounce: 0.25, }; const iconBubbleVariants = { collapsed: { scale: 0, opacity: 0 }, expanded: { scale: 1, opacity: 1 }, }; export interface GooeyInputClassNames { root?: string; filterWrap?: string; buttonRow?: string; trigger?: string; input?: string; bubble?: string; bubbleSurface?: string; } export interface GooeyInputProps { placeholder?: string; className?: string; classNames?: GooeyInputClassNames; /** Collapsed control width in px */ collapsedWidth?: number; /** Expanded control width in px */ expandedWidth?: number; /** Horizontal offset when expanded (px), aligns detached bubble */ expandedOffset?: number; /** Gaussian blur amount for the gooey SVG filter */ gooeyBlur?: number; value?: string; defaultValue?: string; onValueChange?: (value: string) => void; onOpenChange?: (open: boolean) => void; disabled?: boolean; } export function GooeyInput({ placeholder = "Type to search...", className, classNames, collapsedWidth = 115, expandedWidth = 200, expandedOffset = 50, gooeyBlur = 5, value: valueProp, defaultValue = "", onValueChange, onOpenChange, disabled = false, }: GooeyInputProps) { const reactId = useId(); const safeId = reactId.replace(/:/g, ""); const filterId = `gooey-filter-${safeId}`; const iconLayoutId = `gooey-input-icon-${safeId}`; const inputLayoutId = `gooey-input-field-${safeId}`; const inputRef = useRef(null); const prevExpandedRef = useRef(false); const [isExpanded, setIsExpanded] = useState(false); const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue); const isControlled = valueProp !== undefined; const searchText = isControlled ? valueProp : uncontrolledValue; const setSearchText = useCallback( (next: string) => { if (!isControlled) { setUncontrolledValue(next); } onValueChange?.(next); }, [isControlled, onValueChange] ); const setExpanded = useCallback( (next: boolean) => { setIsExpanded(next); onOpenChange?.(next); }, [onOpenChange] ); useEffect(() => { if (isExpanded) { inputRef.current?.focus(); } else if (prevExpandedRef.current) { setSearchText(""); } prevExpandedRef.current = isExpanded; }, [isExpanded, setSearchText]); const buttonVariants = useMemo( () => ({ collapsed: { width: collapsedWidth, marginLeft: 0 }, expanded: { width: expandedWidth, marginLeft: expandedOffset }, }), [collapsedWidth, expandedWidth, expandedOffset] ); const handleExpand = useCallback(() => { if (!disabled) setExpanded(true); }, [disabled, setExpanded]); const handleChange = useCallback( (e: ChangeEvent) => { setSearchText(e.target.value); }, [setSearchText] ); const handleBlur = useCallback(() => { if (!searchText) setExpanded(false); }, [searchText, setExpanded]); const surfaceClass = "bg-foreground text-background shadow-sm ring-1 ring-border/60"; return (
); }