Download CustomCursor.jsx from Gokul08r/Website: direct link, hf CLI and curl.
- Browser
- Download file 7.51 kB
-
https://huggingface.co/spaces/Gokul08r/Website/resolve/main/CustomCursor.jsx
- Command line
-
hf download hf://spaces/Gokul08r/Website/CustomCursor.jsx
-
curl -L -o CustomCursor.jsx https://huggingface.co/spaces/Gokul08r/Website/resolve/main/CustomCursor.jsx
7.51 kB
| // src/components/CustomCursor.jsx | |
| // QUANTUM THEME-REACTIVE CURSOR v5.0 | |
| // β’ Dot follows mouse instantly | |
| // β’ Ring lerps at 0.12 lag | |
| // β’ Trail lerps at 0.05 lag (dreamy) | |
| // β’ Expands on all interactive elements | |
| // β’ Click spawns 8 particle burst | |
| // β’ Magnetic trail dots left behind | |
| // β’ Scan line fires on theme toggle | |
| // β’ ALL colors driven by CSS vars β change instantly on theme toggle | |
| // β’ Zero hardcoded colors in JS | |
| import React, { useEffect, useRef, useCallback } from 'react' | |
| import './CustomCursor.css' | |
| /* ββ Config ββ */ | |
| const RING_LAG = 0.13 | |
| const TRAIL_LAG = 0.055 | |
| const PARTICLE_N = 8 | |
| const TRAIL_EVERY = 4 // px moved before leaving mag dot | |
| const IS_MOBILE = () => window.innerWidth < 768 || !window.matchMedia('(hover:hover)').matches | |
| /* ββ Spawn a single click particle ββ */ | |
| function spawnParticle(x, y) { | |
| const el = document.createElement('div') | |
| el.className = 'c-particle' | |
| el.style.left = `${x}px` | |
| el.style.top = `${y}px` | |
| const angle = Math.random() * Math.PI * 2 | |
| const dist = 20 + Math.random() * 40 | |
| el.style.setProperty('--px', `${Math.cos(angle) * dist}px`) | |
| el.style.setProperty('--py', `${Math.sin(angle) * dist}px`) | |
| document.body.appendChild(el) | |
| setTimeout(() => el.remove(), 750) | |
| } | |
| /* ββ Spawn magnetic trail dot ββ */ | |
| function spawnMagDot(x, y) { | |
| const el = document.createElement('div') | |
| el.className = 'c-mag-dot' | |
| el.style.left = `${x}px` | |
| el.style.top = `${y}px` | |
| document.body.appendChild(el) | |
| setTimeout(() => el.remove(), 600) | |
| } | |
| /* ββ Fire scan line (called from ThemeToggle) ββ */ | |
| function fireScanLine(y) { | |
| const el = document.createElement('div') | |
| el.className = 'c-scan' | |
| el.style.top = `${y}px` | |
| document.body.appendChild(el) | |
| setTimeout(() => el.remove(), 700) | |
| } | |
| // Expose for ThemeToggle to call | |
| if (typeof window !== 'undefined') { | |
| window.__cursorScanLine = fireScanLine | |
| } | |
| export default function CustomCursor() { | |
| const dotRef = useRef(null) | |
| const ringRef = useRef(null) | |
| const trailRef = useRef(null) | |
| const mouse = useRef({ x: 0, y: 0 }) | |
| const ring = useRef({ x: 0, y: 0 }) | |
| const trail = useRef({ x: 0, y: 0 }) | |
| const lastMag = useRef({ x: 0, y: 0 }) | |
| const rafRef = useRef(null) | |
| const active = useRef(false) | |
| /* ββ Expand / shrink ring on hover ββ */ | |
| const expand = useCallback(() => ringRef.current?.classList.add('expand'), []) | |
| const shrink = useCallback(() => ringRef.current?.classList.remove('expand'), []) | |
| /* ββ Spin ring on links ββ */ | |
| const linkEnter = useCallback(() => { | |
| ringRef.current?.classList.add('on-link', 'expand') | |
| }, []) | |
| const linkLeave = useCallback(() => { | |
| ringRef.current?.classList.remove('on-link', 'expand') | |
| }, []) | |
| /* ββ Attach hover listeners to all interactive elements ββ */ | |
| const attachListeners = useCallback(() => { | |
| // Buttons & general interactive | |
| document.querySelectorAll('button, [data-cursor], input, textarea, select, label').forEach(el => { | |
| el.addEventListener('mouseenter', expand, { passive: true }) | |
| el.addEventListener('mouseleave', shrink, { passive: true }) | |
| }) | |
| // Links get spinning ring | |
| document.querySelectorAll('a').forEach(el => { | |
| el.addEventListener('mouseenter', linkEnter, { passive: true }) | |
| el.addEventListener('mouseleave', linkLeave, { passive: true }) | |
| }) | |
| // Cards | |
| document.querySelectorAll('.glass-card, .cert-card, .proj-card, .about-stat').forEach(el => { | |
| el.addEventListener('mouseenter', expand, { passive: true }) | |
| el.addEventListener('mouseleave', shrink, { passive: true }) | |
| }) | |
| }, [expand, shrink, linkEnter, linkLeave]) | |
| useEffect(() => { | |
| if (IS_MOBILE()) return | |
| /* ββ Mouse move ββ */ | |
| const onMove = (e) => { | |
| mouse.current.x = e.clientX | |
| mouse.current.y = e.clientY | |
| // Instant dot placement | |
| if (dotRef.current) { | |
| dotRef.current.style.left = `${e.clientX}px` | |
| dotRef.current.style.top = `${e.clientY}px` | |
| } | |
| // Magnetic trail: leave a dot every TRAIL_EVERY px | |
| const dx = e.clientX - lastMag.current.x | |
| const dy = e.clientY - lastMag.current.y | |
| if (Math.sqrt(dx * dx + dy * dy) > TRAIL_EVERY) { | |
| spawnMagDot(e.clientX, e.clientY) | |
| lastMag.current = { x: e.clientX, y: e.clientY } | |
| } | |
| } | |
| /* ββ Click burst ββ */ | |
| const onClick = (e) => { | |
| if (dotRef.current) dotRef.current.classList.add('clicking') | |
| if (ringRef.current) ringRef.current.classList.add('clicking') | |
| setTimeout(() => { | |
| dotRef.current?.classList.remove('clicking') | |
| ringRef.current?.classList.remove('clicking') | |
| }, 450) | |
| for (let i = 0; i < PARTICLE_N; i++) spawnParticle(e.clientX, e.clientY) | |
| } | |
| /* ββ Cursor hide/show on leave/enter window ββ */ | |
| const onLeave = () => { | |
| if (dotRef.current) dotRef.current.style.opacity = '0' | |
| if (ringRef.current) ringRef.current.style.opacity = '0' | |
| if (trailRef.current) trailRef.current.style.opacity = '0' | |
| } | |
| const onEnter = () => { | |
| if (dotRef.current) dotRef.current.style.opacity = '1' | |
| if (ringRef.current) ringRef.current.style.opacity = '1' | |
| if (trailRef.current) trailRef.current.style.opacity = '0.55' | |
| } | |
| /* ββ RAF loop for lagged elements ββ */ | |
| let frameCount = 0 | |
| const tick = () => { | |
| ring.current.x += (mouse.current.x - ring.current.x) * RING_LAG | |
| ring.current.y += (mouse.current.y - ring.current.y) * RING_LAG | |
| trail.current.x += (mouse.current.x - trail.current.x) * TRAIL_LAG | |
| trail.current.y += (mouse.current.y - trail.current.y) * TRAIL_LAG | |
| if (ringRef.current) { | |
| ringRef.current.style.left = `${ring.current.x}px` | |
| ringRef.current.style.top = `${ring.current.y}px` | |
| } | |
| // Trail updates every 2 frames to feel more ghostly | |
| if (frameCount % 2 === 0 && trailRef.current) { | |
| trailRef.current.style.left = `${trail.current.x}px` | |
| trailRef.current.style.top = `${trail.current.y}px` | |
| } | |
| frameCount++ | |
| rafRef.current = requestAnimationFrame(tick) | |
| } | |
| document.addEventListener('mousemove', onMove, { passive: true }) | |
| document.addEventListener('click', onClick) | |
| document.addEventListener('mouseleave', onLeave, { passive: true }) | |
| document.addEventListener('mouseenter', onEnter, { passive: true }) | |
| // Initial listener attach + reattach on DOM mutations | |
| attachListeners() | |
| const observer = new MutationObserver(() => attachListeners()) | |
| observer.observe(document.body, { childList: true, subtree: true }) | |
| active.current = true | |
| rafRef.current = requestAnimationFrame(tick) | |
| return () => { | |
| document.removeEventListener('mousemove', onMove) | |
| document.removeEventListener('click', onClick) | |
| document.removeEventListener('mouseleave', onLeave) | |
| document.removeEventListener('mouseenter', onEnter) | |
| cancelAnimationFrame(rafRef.current) | |
| observer.disconnect() | |
| } | |
| }, [attachListeners]) | |
| if (typeof window !== 'undefined' && IS_MOBILE()) return null | |
| return ( | |
| <> | |
| {/* Instant-position dot */} | |
| <div ref={dotRef} className="c-dot" aria-hidden="true" /> | |
| {/* Lagged ring */} | |
| <div ref={ringRef} className="c-ring" aria-hidden="true" /> | |
| {/* Dreamy trail */} | |
| <div ref={trailRef} className="c-trail" aria-hidden="true" /> | |
| </> | |
| ) | |
| } |