Multi-Agent Dock This component is a multi-agent dock that allows you to switch between different voice agents. Only one agent can be active at a time. Preview Code Copy the following code to your component file for example dock-example.tsx. "use client"; import { BlocksIcon, CircleIcon, HexagonIcon, OctagonIcon, PentagonIcon, SquareIcon, TriangleIcon, } from "lucide-react"; import { useEffect, useState } from "react"; import { Dock, DockCard, DockCardInner, DockDivider, } from "@/components/ui/dock"; function useIsMobile() { const [isMobile, setIsMobile] = useState(false); useEffect(() => { const userAgent = navigator.userAgent; const isSmall = window.matchMedia("(max-width: 768px)").matches; const isMobile = Boolean( /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.exec( userAgent, ), ); const isDev = process.env.NODE_ENV !== "production"; if (isDev) setIsMobile(isSmall || isMobile); setIsMobile(isSmall && isMobile); }, []); return isMobile; } const gradients = [ "https://products.ls.graphics/mesh-gradients/images/03.-Snowy-Mint_1-p-130x130q80.jpeg", "https://products.ls.graphics/mesh-gradients/images/04.-Hopbush_1-p-130x130q80.jpeg", "https://products.ls.graphics/mesh-gradients/images/06.-Wisteria-p-130x130q80.jpeg", "https://products.ls.graphics/mesh-gradients/images/09.-Light-Sky-Blue-p-130x130q80.jpeg", null, "https://products.ls.graphics/mesh-gradients/images/36.-Pale-Chestnut-p-130x130q80.jpeg", ]; export default function DockAnimation() { const openIcons = [ <CircleIcon key="1" className="h-8 w-8 rounded-full fill-black stroke-black" />, <TriangleIcon key="2" className="h-8 w-8 rounded-full fill-black stroke-black" />, <SquareIcon key="3" className="h-8 w-8 rounded-full fill-black stroke-black" />, <PentagonIcon key="4" className="h-8 w-8 rounded-full fill-black stroke-black" />, null, <BlocksIcon key="7" className="h-8 w-8 rounded-full fill-black stroke-black" />, ]; const isMobile = useIsMobile(); const responsiveOpenIcons = isMobile ? openIcons.slice(3, openIcons.length) : openIcons; const responsiveGradients = isMobile ? gradients.slice(3, gradients.length) : gradients; return ( <div className="flex items-center justify-center w-full py-8" style={{ zIndex: 5, position: "relative" }} > <Dock> {responsiveGradients.map((src, index) => src ? ( <DockCard key={src} id={`${index}`} > <DockCardInner src={src} id={`${index}`}> {responsiveOpenIcons[index]} </DockCardInner> </DockCard> ) : ( <DockDivider key={index} /> ), )} </Dock> </div> ); }Show More Copy the following dock component in to your components folder dock.tsx. "use client"; import { AnimatePresence, type MotionValue, animate, motion, useAnimation, useMotionValue, useSpring, useTransform, } from "framer-motion"; import { type ReactNode, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import useWebRTCAudioSession from "@/hooks/use-webrtc"; import { cn } from "@/lib/utils"; interface DockContextType { width: number; hovered: boolean; setIsZooming: (value: boolean) => void; zoomLevel: MotionValue<number>; mouseX: MotionValue<number>; animatingIndexes: number[]; setAnimatingIndexes: (indexes: number[]) => void; activeCardId: number | null; setActiveCardId: (id: number | null) => void; } const DockContext = createContext<DockContextType>({ width: 0, hovered: false, setIsZooming: () => {}, zoomLevel: null as any, mouseX: null as any, animatingIndexes: [], setAnimatingIndexes: () => {}, activeCardId: null, setActiveCardId: () => {}, }); // Initial width for the dock const INITIAL_WIDTH = 48; // Custom hook to use Dock context const useDock = () => useContext(DockContext); // Props for the Dock component interface DockProps { className?: string; children: ReactNode; // React children to be rendered within the dock } // Main Dock component: orchestrating the dock's animation behavior function Dock({ className, children }: DockProps) { const [hovered, setHovered] = useState(false); const [width, setWidth] = useState(0); const dockRef = useRef<HTMLDivElement>(null); const isZooming = useRef(false); const [animatingIndexes, setAnimatingIndexes] = useState<number[]>([]); const [activeCardId, setActiveCardId] = useState<number | null>(null); const setIsZooming = useCallback((value: boolean) => { isZooming.current = value; setHovered(!value); }, []); const zoomLevel = useMotionValue(1); useWindowResize(() => { setWidth(dockRef.current?.clientWidth || 0); }); const mouseX = useMotionValue(Number.POSITIVE_INFINITY); return ( <DockContext.Provider value={{ hovered, setIsZooming, width, zoomLevel, mouseX, animatingIndexes, setAnimatingIndexes, activeCardId, setActiveCardId, }} > <motion.div ref={dockRef} className={cn( "-translate-x-1/2 absolute bottom-4 left-1/2 flex h-14 transform items-end gap-3 rounded-xl bg-opacity-90 p-2", " bg-neutral-50 p-2 no-underline shadow-sm transition-colors dark:bg-neutral-900 dark:hover:bg-neutral-800/80 hover:bg-neutral-100 ", "shadow-[0px_1px_1px_0px_rgba(0,0,0,0.05),0px_1px_1px_0px_rgba(255,252,240,0.5)_inset,0px_0px_0px_1px_hsla(0,0%,100%,0.1)_inset,0px_0px_1px_0px_rgba(28,27,26,0.5)]", "dark:shadow-[0_1px_0_0_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(255,255,255,0.03)_inset,0_0_0_1px_rgba(0,0,0,0.1),0_2px_2px_0_rgba(0,0,0,0.1),0_4px_4px_0_rgba(0,0,0,0.1),0_8px_8px_0_rgba(0,0,0,0.1)]", className, )} onMouseMove={(e) => { mouseX.set(e.pageX); if (!isZooming.current) { setHovered(true); } }} onMouseLeave={() => { mouseX.set(Number.POSITIVE_INFINITY); setHovered(false); }} style={{ x: "-50%", scale: zoomLevel, }} > {children} </motion.div> </DockContext.Provider> ); } // Props for the DockCardInner component interface DockCardInnerProps { src: string; // Source URL for the image id: string; // Unique identifier for the card children?: ReactNode; // Optional children for the card } // DockCardInner component to display images and handle animation states function DockCardInner({ src, id, children }: DockCardInnerProps) { const { animatingIndexes } = useDock(); // Access the Dock context to get the animating indexes. This determines which cards are currently animating. return ( <span className="relative z-0 flex max-h-8 w-full items-center justify-center overflow-hidden rounded-md"> {/* Background image with a blur effect to give a depth illusion */} {/* <motion.img className="absolute z-10 translate-y-2.5 scale-125 transform opacity-40 blur-md filter " src={src} alt="" /> */} {/* AnimatePresence component to handle the entrance and exit animations of children - in our case, the "openIcon" */} <AnimatePresence> {animatingIndexes.includes(Number.parseInt(id)) && children ? ( <motion.div className="relative z-0 h-full w-full rounded-full" initial={{ scale: 0, opacity: 0, filter: "blur(4px)" }} animate={{ scale: 1, opacity: 1, filter: "blur(0px)", transition: { type: "spring", delay: 0.2 }, // Animation to spring into place with a delay so our layoutId animations can be smooth }} exit={{ scale: 0, opacity: 0, filter: "blur(4px)", transition: { duration: 0 }, // Exit animation with no delay }} > <div className="flex h-full w-full flex-col items-center justify-center"> {/* Render the openIcon */} {children} </div> </motion.div> ) : null} </AnimatePresence> {/* Another AnimatePresence to handle layout animations */} <AnimatePresence mode="popLayout"> {!animatingIndexes.includes(Number.parseInt(id)) ? ( <motion.img layoutId={id} // Unique identifier for layout animations className="relative z-0 h-1/2 w-1/2 rounded-full border border-black/30 dark:border-white/10" src={src} alt="" /> ) : null} </AnimatePresence> </span> ); } // Props for the DockCard component interface DockCardProps { children: ReactNode; id: string; } // DockCard component: manages individual card behavior within the dock function DockCard({ children, id }: DockCardProps) { const cardRef = useRef<HTMLButtonElement>(null); const [elCenterX, setElCenterX] = useState(0); const dock = useDock(); const { handleStartStopClick } = useWebRTCAudioSession('alloy'); const size = useSpring(INITIAL_WIDTH, { stiffness: 320, damping: 20, mass: 0.1, }); const opacity = useSpring(0, { stiffness: 300, damping: 20, }); useEffect(() => { const { x } = cardRef.current?.getBoundingClientRect() || { x: 0 }; setElCenterX(x + 24); // 24 is the half of INITIAL_WIDTH (48 / 2), centering the element }, []); const isAnimating = useRef(false); const controls = useAnimation(); const timeoutRef = useRef<number | null>(null); const handleClick = () => { if ( dock.activeCardId !== null && dock.activeCardId !== Number.parseInt(id) ) { return; } if (!isAnimating.current) { isAnimating.current = true; dock.setAnimatingIndexes([...dock.animatingIndexes, Number.parseInt(id)]); opacity.set(0.5); controls.start({ y: -24, transition: { repeat: Number.POSITIVE_INFINITY, repeatType: "reverse", duration: 0.5, }, }); dock.setActiveCardId(Number.parseInt(id)); handleStartStopClick(); } else { isAnimating.current = false; dock.setAnimatingIndexes( dock.animatingIndexes.filter((index) => index !== Number.parseInt(id)), ); opacity.set(0); controls.start({ y: 0, transition: { duration: 0.5 }, }); dock.setActiveCardId(null); handleStartStopClick(); } }; useEffect(() => { // biome-ignore lint/style/noNonNullAssertion: <explanation> return () => clearTimeout(timeoutRef.current!); }, []); const distance = useTransform(dock.mouseX, (val) => { const bounds = cardRef.current?.getBoundingClientRect() ?? { x: 0, width: 0, }; return val - bounds.x - bounds.width / 2; }); const widthSync = useTransform(distance, [-150, 0, 150], [40, 80, 40]); const width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12, }); return ( <div className="flex flex-col items-center gap-1" key={id}> <motion.button ref={cardRef} className="aspect-square w-full rounded-lg border border-black/5 border-opacity-10 bg-neutral-100 brightness-90 saturate-90 transition-filter duration-200 dark:border-white/5 dark:bg-neutral-800 hover:brightness-112 hover:saturate-100" onClick={handleClick} style={{ width }} animate={controls} whileTap={{ scale: 0.95 }} > {children} </motion.button> <AnimatePresence mode="popLayout"> {dock.animatingIndexes.includes(Number.parseInt(id)) ? ( <motion.div key={id} layoutId={id} className="rounded-full" style={{ opacity }} > <motion.div exit={{ transition: { duration: 0 } }} className="h-1.5 w-1.5 rounded-full bg-black dark:bg-white" style={{ opacity }} /> </motion.div> ) : null} </AnimatePresence> </div> ); } // Divider component for the dock function DockDivider() { return ( <motion.div className="flex h-full cursor-ns-resize items-center p-1.5" drag="y" dragConstraints={{ top: -100, bottom: 50 }} > <span className="h-full w-0.5 rounded bg-neutral-800/10 dark:bg-neutral-100/10 " /> </motion.div> ); } type UseWindowResizeCallback = (width: number, height: number) => void; // Custom hook to handle window resize events and invoke a callback with the new dimensions function useWindowResize(callback: UseWindowResizeCallback) { // Create a stable callback reference to ensure the latest callback is always used const callbackRef = useCallbackRef(callback); useEffect(() => { // Function to handle window resize and call the provided callback with updated dimensions const handleResize = () => { callbackRef(window.innerWidth, window.innerHeight); }; // Initial call to handleResize to capture the current window size handleResize(); // Adding event listener for window resize events window.addEventListener("resize", handleResize); // Cleanup function to remove the event listener when the component unmounts or dependencies change return () => { window.removeEventListener("resize", handleResize); }; }, [callbackRef]); // Dependency array includes the stable callback reference } // Custom hook to create a stable callback reference function useCallbackRef<T extends (...args: any[]) => any>(callback: T): T { // Use a ref to store the callback const callbackRef = useRef(callback); // Update the ref with the latest callback whenever it changes useEffect(() => { callbackRef.current = callback; }); // Return a memoized version of the callback that always uses the latest callback stored in the ref return useMemo(() => ((...args) => callbackRef.current?.(...args)) as T, []); } // Interface for mouse position options interface MousePositionOptions { onChange?: (position: { value: { x: number; y: number } }) => void; } // Custom hook to track mouse position and animate values accordingly export function useMousePosition( options: MousePositionOptions = {}, // Options to customize behavior, including an onChange callback deps: readonly any[] = [], // Dependencies array export default Docke when the effect should re-run ) { const { onChange } = options; // Destructure onChange from options for use in the effect // Create motion values for x and y coordinates, initialized to 0 const x = useMotionValue(0); const y = useMotionValue(0); useEffect(() => { // Function to handle mouse move events, animating the x and y motion values to the current mouse coordinates const handleMouseMove = (event: MouseEvent) => { animate(x, event.clientX); animate(y, event.clientY); }; // Function to handle changes in the motion values, calling the onChange callback if it exists const handleChange = () => { if (onChange) { onChange({ value: { x: x.get(), y: y.get() } }); } }; // Subscribe to changes in the x and y motion values const unsubscribeX = x.on("change", handleChange); const unsubscribeY = y.on("change", handleChange); // Add event listener for mouse move events window.addEventListener("mousemove", handleMouseMove); // Cleanup function to remove event listener and unsubscribe from motion value changes return () => { window.removeEventListener("mousemove", handleMouseMove); unsubscribeX(); unsubscribeY(); }; }, [x, y, onChange, ...deps]); // Dependency array includes x, y, onChange, and any additional dependencies // Memoize and return the motion values for x and y coordinates return useMemo( () => ({ x, // Motion value for x coordinate y, // Motion value for y coordinate }), [x, y], // Dependencies for the memoized return value ); } export { Dock, DockCard, DockCardInner, DockDivider, useDock }; Show More Usage Import the component in your file and then use it in your page. Ensure the use-webrtc hook is copied over to your project. Note: This component uses Tailwind CSS, make sure to have it installed in your project. import DockExample from "@/components/openai-blocks/dock_example"; export default function Home() { return ( <main className="flex items-center justify-center h-screen"> <DockExample /> </main> ); }Show More