first commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const HOVER_SELECTOR =
|
||||
'a, button, [role="button"], input, textarea, select, summary, [data-cursor-hover]';
|
||||
|
||||
export function CustomCursor() {
|
||||
const dotRef = useRef<HTMLDivElement>(null);
|
||||
const ringRef = useRef<HTMLDivElement>(null);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only enable on desktop pointers (>= 900px and fine pointer)
|
||||
const mq = window.matchMedia('(min-width: 900px) and (pointer: fine)');
|
||||
const apply = () => {
|
||||
const on = mq.matches;
|
||||
setEnabled(on);
|
||||
document.documentElement.classList.toggle('has-cursor', on);
|
||||
};
|
||||
apply();
|
||||
mq.addEventListener('change', apply);
|
||||
return () => mq.removeEventListener('change', apply);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
let dotX = window.innerWidth / 2;
|
||||
let dotY = window.innerHeight / 2;
|
||||
let ringX = dotX;
|
||||
let ringY = dotY;
|
||||
let raf = 0;
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
dotX = e.clientX;
|
||||
dotY = e.clientY;
|
||||
};
|
||||
|
||||
const tick = () => {
|
||||
// Ring lags the dot — trailing effect.
|
||||
ringX += (dotX - ringX) * 0.18;
|
||||
ringY += (dotY - ringY) * 0.18;
|
||||
if (dotRef.current) {
|
||||
dotRef.current.style.transform = `translate3d(${dotX}px, ${dotY}px, 0) translate(-50%, -50%)`;
|
||||
}
|
||||
if (ringRef.current) {
|
||||
ringRef.current.style.transform = `translate3d(${ringX}px, ${ringY}px, 0) translate(-50%, -50%)`;
|
||||
}
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const onOver = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
const isHover = !!target?.closest(HOVER_SELECTOR);
|
||||
ringRef.current?.classList.toggle('cursor-ring--hover', isHover);
|
||||
dotRef.current?.classList.toggle('cursor-dot--hover', isHover);
|
||||
};
|
||||
|
||||
const onDown = () => ringRef.current?.classList.add('cursor-ring--down');
|
||||
const onUp = () => ringRef.current?.classList.remove('cursor-ring--down');
|
||||
const onLeave = () => {
|
||||
ringRef.current?.classList.add('cursor--hidden');
|
||||
dotRef.current?.classList.add('cursor--hidden');
|
||||
};
|
||||
const onEnter = () => {
|
||||
ringRef.current?.classList.remove('cursor--hidden');
|
||||
dotRef.current?.classList.remove('cursor--hidden');
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseover', onOver);
|
||||
window.addEventListener('mousedown', onDown);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
document.addEventListener('mouseleave', onLeave);
|
||||
document.addEventListener('mouseenter', onEnter);
|
||||
raf = requestAnimationFrame(tick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseover', onOver);
|
||||
window.removeEventListener('mousedown', onDown);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
document.removeEventListener('mouseleave', onLeave);
|
||||
document.removeEventListener('mouseenter', onEnter);
|
||||
cancelAnimationFrame(raf);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
if (!enabled) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
.cursor-dot,
|
||||
.cursor-ring {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
will-change: transform;
|
||||
transition:
|
||||
width 0.25s ease,
|
||||
height 0.25s ease,
|
||||
background 0.25s ease,
|
||||
border-color 0.25s ease,
|
||||
opacity 0.25s ease;
|
||||
}
|
||||
.cursor-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #38bdf8;
|
||||
box-shadow: 0 0 14px rgba(56, 189, 248, 0.8);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
.cursor-ring {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 999px;
|
||||
border: 1.5px solid rgba(56, 189, 248, 0.55);
|
||||
}
|
||||
.cursor-dot--hover {
|
||||
background: #e879f9;
|
||||
box-shadow: 0 0 18px rgba(232, 121, 249, 0.85);
|
||||
}
|
||||
.cursor-ring--hover {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-color: rgba(232, 121, 249, 0.7);
|
||||
background: rgba(232, 121, 249, 0.05);
|
||||
}
|
||||
.cursor-ring--down {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.cursor--hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
`}</style>
|
||||
<div ref={ringRef} className="cursor-ring" aria-hidden />
|
||||
<div ref={dotRef} className="cursor-dot" aria-hidden />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user