The idea is to create an entering variant that at the end of the animation will become a hovering variant to my custom cursor. It worked, but there is a bug that if the mouse leaves the menu icon before the entering animation ends, it bugs and the hovering animation is out of the icon menu. To fix this I'm trying to create setTimeout and clearTimeout as a callback to programmatically use in React events. I want to clear setTimeout after its execution and before the callback is executed. If the user takes the mouse off before the setTimeout delay, the clear callback will be called to clear the setTimeout and cancel the hovering animation before the delay time finishes. But that snippet is not working. Any ideas how I can make this animation transition with framer motion?
the code idea:
function useDelay(cb: () => void, delay: number) {
const timer = () => setTimeout(cb, delay);
const clearTimer = () => clearTimeout(timer());
return [timer, clearTimer];
}
full code:
//navbar.tsx
import React, { useState } from 'react';
import Image from 'next/image';
import Logo from '../public/logo.svg';
import { useRecoilState } from 'recoil';
import { cursorVariantAtom } from '../recoil/atoms';
export const Navbar = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [hover, setHover] = useState(false);
const toggleMenu = () => setIsMenuOpen((s) => !s);
const toggleHover = () => setHover((s) => !s);
const [_, setCursorVariant] = useRecoilState(cursorVariantAtom);
function useDelay(cb: () => void, delay: number) {
const timer = () => setTimeout(cb, delay);
const clearTimer = () => clearTimeout(timer());
return [timer, clearTimer];
}
const [delay, clear] = useDelay(
() => setCursorVariant('homeMenuIconHovering'),
250
);
return (
<div
className='relative group group-hover:bg-transparent z-40 flex items-center justify-end w-[70px] h-[70px]'
onClick={toggleMenu}
onMouseEnter={() => {
toggleHover();
setCursorVariant('homeMenuIconEntering');
delay();
}}
onMouseLeave={() => {
clear();
toggleHover();
setCursorVariant('default');
}}
onMouseDown={() => setCursorVariant('clicked')}
onMouseUp={() => setCursorVariant('homeMenuIconEntering')}
>
<div className='relative'>
<span
className={`absolute -translate-x-full left-0 h-[4px] ${
hover ? 'bg-primary' : 'bg-white'
}
ease-in-out ${
isMenuOpen
? 'rotate-45 w-[30px] md:w-[35px] bottom-0 duration-500'
: 'w-[30px] md:w-[45px] bottom-[2.5px] duration-[200ms]'
}
`}
/>
<span
className={`absolute -translate-x-full left-0 h-[4px] ${
hover ? 'bg-primary' : 'bg-white'
}
ease-in-out duration-[400ms] ${
isMenuOpen
? '-rotate-45 w-[30px] md:w-[35px] bottom-0'
: 'w-[20px] md:w-[30px] -bottom-[7.5px]'
}
`}
/>
<span
className={`absolute -translate-x-full left-0 h-[4px] ${
hover ? 'bg-primary' : 'bg-white'
}
ease-in-out duration-[600ms] ${
isMenuOpen
? '-rotate-45 w-[30px] md:w-[35px] bottom-0 opacity-0 duration-75'
: 'w-[10px] md:w-[15px] -bottom-[16.5px]'
}
`}
/>
</div>
</div>
</nav>
);
};
//variants.ts
export interface MousePosition {
x: number;
y: number;
}
export const cursorVariants: (mousePosition: MousePosition) => Variants = (
mousePosition: MousePosition
) => {
return {
default: (variant: CursorVariants) => {
const props = {
x: mousePosition.x - 4,
y: mousePosition.y - 4,
border: 'none',
};
};
};
export const cursorStyleVariants: Variants = {
default: {
width: 8,
height: 8,
border: 'none',
transition: {
type: 'tween',
duration: 0.25,
ease: [0.92, 0.96, 0.98, 0.94],
},
},
clicked: {
width: [8, 32, 8],
height: [8, 32, 8],
border: '2px solid rgb(237 12 50)',
background: 'transparent',
transition: {
type: 'tween',
duration: 0.45,
ease: [1.5, 1.6, 1.4, 0.97],
},
},
homeMenuIconEntering: {
width: 8 * 7,
height: 8 * 7,
border: '2px solid rgb(237 12 50)',
background: 'transparent',
transition: {
type: 'tween',
duration: 0.25,
ease: [1.5, 1.6, 1.4, 0.97],
},
},
homeMenuIconHovering: {
width: [8 * 7, 8 * 8, 8 * 7],
height: [8 * 7, 8 * 8, 8 * 7],
border: '2px solid rgb(237 12 50)',
background: 'transparent',
transition: {
type: 'tween',
duration: 0.4,
ease: [1.5, 1.6, 1.4, 0.97],
repeat: Infinity,
},
}
//layout.tsx
import React, { useEffect, useState } from 'react';
import { motion, Variants } from 'framer-motion';
import { useRecoilState } from 'recoil';
import { cursorVariantAtom, CursorVariants } from '../recoil/atoms';
import {
cursorStyleVariants,
cursorVariants,
MousePosition,
} from '../helpers/variants';
interface Props {
children: React.ReactNode;
}
const Layout = ({ children }: Props) => {
const [mousePosition, setMousePosition] = useState<MousePosition>({
x: 0,
y: 0,
});
const [cursorVariant, setCursorVariant] = useRecoilState(cursorVariantAtom);
useEffect(() => {
const mouseMove = (e) => {
setMousePosition({
x: e.clientX,
y: e.clientY,
});
};
window.addEventListener('mousemove', mouseMove);
}, []);
return (
// layout all site
<div
onMouseEnter={() => setCursorVariant('default')}
className='bg-layout-2 cursor-none flex flex-col justify-between max-h-full h-screen text-white px-5 pb-4 md:px-6 md:pb-10 overflow-hidden'
>
{/* cursor movement */}
<motion.div
variants={cursorVariants(mousePosition)}
animate='default'
custom={cursorVariant}
transition={{
type: 'spring',
mass: 0.1,
stiffness: 400,
bounce: 0.05,
}}
whileTap={{ scale: 0.5 }}
className='z-[100] h-fit w-fit rounded-[50%] pointer-events-none fixed top-0 left-0 hidden md:block'
>
{/* cursor styles */}
<motion.div
variants={cursorStyleVariants}
animate={cursorVariant}
className='bg-primary -translate-x-1/2 -translate-y-1/2 rounded-[50%] pointer-events-none fixed top-0 left-0 hidden md:block'
/>
</motion.div>
{/* all sites components */}
{children}
</div>
);
};
export default Layout;
//atoms.ts
import { atom } from 'recoil';
export type CursorVariants =
| 'default'
| 'homeMenuIconEntering'
| 'homeMenuIconHovering'
export const cursorVariantAtom = atom({
key: 'cursor-variant',
default: 'default' as CursorVariants,
});