Re-create the Apple Vision OS window resize handle animation with real resizing functionality.
import { useCallback, useRef, useState } from "react"; import { AnimatePresence, motion, MotionConfig, useDragControls, } from "motion/react"; const variants = { initial: { opacity: 0, pathLength: 0, }, animate: { opacity: 1, pathLength: 1, }, exit: { opacity: 0, pathLength: 0, }, }; const PADDING = 8; const MIN_SIZE = 100; export default function AppleVisionResizeHandle() { const [draggingResizingCorner, setDraggingResizingCorner] = useState< "br" | "bl" | undefined >(undefined); const activeResizingHandle = useRef<"br" | "bl" | undefined>(undefined); const [size, setSize] = useState({ width: 100, height: 100 }); const [isDragging, setIsDragging] = useState(false); const containerRef = useRef<HTMLDivElement>(null); const startPosRef = useRef({ x: 0, y: 0, width: 0, height: 0 }); const dragControls = useDragControls(); const handlePointerMove = useCallback((e: PointerEvent) => { const deltaX = e.clientX - startPosRef.current.x; const deltaY = e.clientY - startPosRef.current.y; // Calculate new dimensions based on which corner is being dragged // Using a 2x multiplier as container expands in both directions let newWidth = startPosRef.current.width; let newHeight = startPosRef.current.height; if (activeResizingHandle.current === "br") { newWidth = Math.max(MIN_SIZE, startPosRef.current.width + deltaX * 2); newHeight = Math.max(MIN_SIZE, startPosRef.current.height + deltaY * 2); } else if (activeResizingHandle.current === "bl") { newWidth = Math.max(MIN_SIZE, startPosRef.current.width - deltaX * 2); newHeight = Math.max(MIN_SIZE, startPosRef.current.height + deltaY * 2); } // Limit the size to the container if (containerRef.current) { const containerRect = containerRef.current.getBoundingClientRect(); const maxWidth = containerRect.width - PADDING; const maxHeight = containerRect.height - PADDING; newWidth = Math.min(newWidth, maxWidth); newHeight = Math.min(newHeight, maxHeight); } setSize({ width: newWidth, height: newHeight }); }, []); const handlePointerUp = useCallback(() => { setIsDragging(false); setDraggingResizingCorner(undefined); activeResizingHandle.current = undefined; window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); }, [handlePointerMove]); const handlePointerDown = useCallback( (e: React.PointerEvent, handle: "br" | "bl") => { setIsDragging(true); setDraggingResizingCorner(handle); activeResizingHandle.current = handle; // Store initial position and size startPosRef.current = { x: e.clientX, y: e.clientY, width: size.width, height: size.height, }; // Add event listeners for dragging window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); }, [size, handlePointerMove, handlePointerUp] ); return ( <MotionConfig transition={{ duration: 0.375, type: "spring", bounce: 0 }}> <div className="relative flex h-screen w-screen items-center justify-center" style={{ background: "url(https://images.unsplash.com/photo-1564078516393-cf04bd966897?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D) no-repeat center bottom", backgroundSize: "cover", }} > <div className="absolute inset-24 flex items-center justify-center" ref={containerRef} > <motion.div drag dragConstraints={containerRef} dragControls={dragControls} dragMomentum={false} dragListener={false} className="relative rounded-[20px] bg-neutral-500/30 shadow-[inset_0_0_5px_rgba(255,255,255,0.5)] backdrop-blur-md select-none" style={{ width: `${size.width}px`, height: `${size.height}px`, transition: isDragging ? "none" : "width 0.2s, height 0.2s", }} > <div className="absolute inset-4 flex flex-col items-center justify-center gap-y-2 overflow-hidden rounded-[12px]"> <div className="relative z-10 flex flex-col items-center justify-center gap-y-2"> <div className="text-xs text-neutral-200"> {Math.round(size.width)} × {Math.round(size.height)} </div> </div> </div> <AnimatePresence> {(() => { if (!draggingResizingCorner) return null; switch (draggingResizingCorner) { // bottom right corner resize handle case "br": return ( <motion.div key={draggingResizingCorner} className="absolute right-0 bottom-0 size-10 overflow-visible" initial={{ scale: 1, x: 16, y: 16 }} animate={{ scale: isDragging ? 0.9 : 1, x: 16, y: 16 }} > <motion.svg viewBox="0 0 30 30" className="size-full" fill="none" > <motion.path d="M10,24 Q22,22 24,10" stroke="white" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.7" variants={variants} initial="initial" animate="animate" exit="exit" /> </motion.svg> </motion.div> ); // bottom left corner resize handle case "bl": return ( <motion.div key={draggingResizingCorner} className="absolute bottom-0 left-0 size-10 overflow-visible" initial={{ scale: 1, x: -16, y: 16 }} animate={{ scale: isDragging ? 0.9 : 1, x: -16, y: 16 }} > <motion.svg viewBox="0 0 30 30" className="size-full" fill="none" > <motion.path d="M20,24 Q8,22 6,10" stroke="white" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.7" variants={variants} initial="initial" animate="animate" exit="exit" /> </motion.svg> </motion.div> ); } })()} {/* bottom middle drag handle */} {draggingResizingCorner !== "bl" && draggingResizingCorner !== "br" && ( <div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-[32px]"> <motion.div key={draggingResizingCorner} className="h-full w-20 touch-none overflow-visible opacity-70" onPointerDown={(event) => dragControls.start(event, { snapToCursor: false }) } exit={{ scale: 1, opacity: 0 }} whileHover={{ scale: 1, opacity: 1 }} whileTap={{ scale: 0.9, opacity: 1 }} > <svg viewBox="0 0 80 30" className="size-full" fill="none" > <path d="M4,9 L76,9" stroke="white" strokeWidth="5.5" strokeLinecap="round" strokeLinejoin="round" fill="none" /> </svg> </motion.div> </div> )} </AnimatePresence> {/* pointer capture areas */} {/* bottom right corner resize handle capture area */} <div className="absolute right-0 bottom-0 size-10 translate-x-4 translate-y-4 cursor-default" onPointerEnter={useCallback( () => !isDragging && setDraggingResizingCorner("br"), [isDragging] )} onPointerLeave={useCallback( () => !isDragging && setDraggingResizingCorner(undefined), [isDragging] )} onPointerDown={(event) => handlePointerDown(event, "br")} style={{ touchAction: "none" }} /> {/* bottom left corner resize handle capture area */} <div className="absolute bottom-0 left-0 size-10 -translate-x-4 translate-y-4 cursor-default" onPointerEnter={useCallback( () => !isDragging && setDraggingResizingCorner("bl"), [isDragging] )} onPointerLeave={useCallback( () => !isDragging && setDraggingResizingCorner(undefined), [isDragging] )} onPointerDown={(event) => handlePointerDown(event, "bl")} style={{ touchAction: "none" }} /> </motion.div> </div> <p className="absolute bottom-6 px-4 text-center text-xs text-neutral-400 select-none"> Drag the bottom corners to resize the card. Drag the bottom middle to move the card. </p> </div> </MotionConfig> ); }