From e11bf49d6e36394a22771601cd3882f6e585f702 Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 20 Jun 2026 23:12:18 +0300 Subject: [PATCH 1/2] feat(TargetCursor): add cursorColor and cursorColorOnTarget props --- .../Animations/TargetCursor/TargetCursor.jsx | 63 ++++++++-- .../Animations/TargetCursor/TargetCursor.jsx | 74 ++++++++--- .../Animations/TargetCursor/TargetCursor.tsx | 116 +++++++++++++++--- .../Animations/TargetCursor/TargetCursor.tsx | 76 +++++++++--- 4 files changed, 274 insertions(+), 55 deletions(-) diff --git a/src/content/Animations/TargetCursor/TargetCursor.jsx b/src/content/Animations/TargetCursor/TargetCursor.jsx index 05ce3eb37..f9b8d63d5 100644 --- a/src/content/Animations/TargetCursor/TargetCursor.jsx +++ b/src/content/Animations/TargetCursor/TargetCursor.jsx @@ -37,7 +37,9 @@ const TargetCursor = ({ spinDuration = 2, hideDefaultCursor = true, hoverDuration = 0.2, - parallaxOn = true + parallaxOn = true, + cursorColor = '#ffffff', + cursorColorOnTarget }) => { const cursorRef = useRef(null); const cornersRef = useRef(null); @@ -217,12 +219,27 @@ const TargetCursor = ({ activeTarget = target; const corners = Array.from(cornersRef.current); - corners.forEach(corner => gsap.killTweensOf(corner)); + corners.forEach(corner => gsap.killTweensOf(corner, 'x,y')); gsap.killTweensOf(cursorRef.current, 'rotation'); spinTl.current?.pause(); gsap.set(cursorRef.current, { rotation: 0 }); + if (cursorColorOnTarget) { + gsap.to(corners, { + borderColor: cursorColorOnTarget, + duration: 0.15, + ease: 'power2.out' + }); + if (dotRef.current) { + gsap.to(dotRef.current, { + backgroundColor: cursorColorOnTarget, + duration: 0.15, + ease: 'power2.out' + }); + } + } + const rect = target.getBoundingClientRect(); const { borderWidth, cornerSize } = constants; const { x: offsetX, y: offsetY } = getOffset(); @@ -262,9 +279,24 @@ const TargetCursor = ({ gsap.set(activeStrengthRef, { current: 0, overwrite: true }); activeTarget = null; + if (cursorColorOnTarget && cornersRef.current) { + gsap.to(Array.from(cornersRef.current), { + borderColor: cursorColor, + duration: 0.15, + ease: 'power2.out' + }); + if (dotRef.current) { + gsap.to(dotRef.current, { + backgroundColor: cursorColor, + duration: 0.15, + ease: 'power2.out' + }); + } + } + if (cornersRef.current) { const corners = Array.from(cornersRef.current); - gsap.killTweensOf(corners); + gsap.killTweensOf(corners, 'x,y'); const { cornerSize } = constants; const positions = [ { x: -cornerSize * 1.5, y: -cornerSize * 1.5 }, @@ -344,7 +376,18 @@ const TargetCursor = ({ targetCornerPositionsRef.current = null; activeStrengthRef.current = 0; }; - }, [targetSelector, spinDuration, moveCursor, constants, hideDefaultCursor, isMobile, hoverDuration, parallaxOn]); + }, [ + targetSelector, + spinDuration, + moveCursor, + constants, + hideDefaultCursor, + isMobile, + hoverDuration, + parallaxOn, + cursorColor, + cursorColorOnTarget + ]); useEffect(() => { if (isMobile || !cursorRef.current || !spinTl.current) return; @@ -362,13 +405,13 @@ const TargetCursor = ({ return (
-
-
-
-
-
+
+
+
+
+
); }; -export default TargetCursor; +export default TargetCursor; \ No newline at end of file diff --git a/src/tailwind/Animations/TargetCursor/TargetCursor.jsx b/src/tailwind/Animations/TargetCursor/TargetCursor.jsx index e3a89e2a9..aa689a819 100644 --- a/src/tailwind/Animations/TargetCursor/TargetCursor.jsx +++ b/src/tailwind/Animations/TargetCursor/TargetCursor.jsx @@ -36,7 +36,9 @@ const TargetCursor = ({ spinDuration = 2, hideDefaultCursor = true, hoverDuration = 0.2, - parallaxOn = true + parallaxOn = true, + cursorColor = '#ffffff', + cursorColorOnTarget }) => { const cursorRef = useRef(null); const cornersRef = useRef(null); @@ -209,11 +211,26 @@ const TargetCursor = ({ activeTarget = target; const corners = Array.from(cornersRef.current); - corners.forEach(corner => gsap.killTweensOf(corner)); + corners.forEach(corner => gsap.killTweensOf(corner, 'x,y')); gsap.killTweensOf(cursorRef.current, 'rotation'); spinTl.current?.pause(); gsap.set(cursorRef.current, { rotation: 0 }); + if (cursorColorOnTarget) { + gsap.to(corners, { + borderColor: cursorColorOnTarget, + duration: 0.15, + ease: 'power2.out' + }); + if (dotRef.current) { + gsap.to(dotRef.current, { + backgroundColor: cursorColorOnTarget, + duration: 0.15, + ease: 'power2.out' + }); + } + } + const rect = target.getBoundingClientRect(); const { borderWidth, cornerSize } = constants; const { x: offsetX, y: offsetY } = getOffset(); @@ -247,9 +264,25 @@ const TargetCursor = ({ targetCornerPositionsRef.current = null; gsap.set(activeStrengthRef, { current: 0, overwrite: true }); activeTarget = null; + + if (cursorColorOnTarget && cornersRef.current) { + gsap.to(Array.from(cornersRef.current), { + borderColor: cursorColor, + duration: 0.15, + ease: 'power2.out' + }); + if (dotRef.current) { + gsap.to(dotRef.current, { + backgroundColor: cursorColor, + duration: 0.15, + ease: 'power2.out' + }); + } + } + if (cornersRef.current) { const corners = Array.from(cornersRef.current); - gsap.killTweensOf(corners); + gsap.killTweensOf(corners, 'x,y'); const { cornerSize } = constants; const positions = [ { x: -cornerSize * 1.5, y: -cornerSize * 1.5 }, @@ -313,7 +346,18 @@ const TargetCursor = ({ targetCornerPositionsRef.current = null; activeStrengthRef.current = 0; }; - }, [targetSelector, spinDuration, moveCursor, constants, hideDefaultCursor, isMobile, hoverDuration, parallaxOn]); + }, [ + targetSelector, + spinDuration, + moveCursor, + constants, + hideDefaultCursor, + isMobile, + hoverDuration, + parallaxOn, + cursorColor, + cursorColorOnTarget + ]); useEffect(() => { if (isMobile || !cursorRef.current || !spinTl.current) return; @@ -337,27 +381,27 @@ const TargetCursor = ({ >
); }; -export default TargetCursor; +export default TargetCursor; \ No newline at end of file diff --git a/src/ts-default/Animations/TargetCursor/TargetCursor.tsx b/src/ts-default/Animations/TargetCursor/TargetCursor.tsx index 1c1761bb6..ed804950c 100644 --- a/src/ts-default/Animations/TargetCursor/TargetCursor.tsx +++ b/src/ts-default/Animations/TargetCursor/TargetCursor.tsx @@ -38,6 +38,8 @@ export interface TargetCursorProps { hideDefaultCursor?: boolean; hoverDuration?: number; parallaxOn?: boolean; + cursorColor?: string; + cursorColorOnTarget?: string; } const TargetCursor: React.FC = ({ @@ -45,7 +47,9 @@ const TargetCursor: React.FC = ({ spinDuration = 2, hideDefaultCursor = true, hoverDuration = 0.2, - parallaxOn = true + parallaxOn = true, + cursorColor = '#ffffff', + cursorColorOnTarget }) => { const cursorRef = useRef(null); const cornersRef = useRef | null>(null); @@ -68,12 +72,23 @@ const TargetCursor: React.FC = ({ return (hasTouchScreen && isSmallScreen) || isMobileUserAgent; }, []); - const constants = useMemo(() => ({ borderWidth: 3, cornerSize: 12 }), []); + const constants = useMemo( + () => ({ + borderWidth: 3, + cornerSize: 12 + }), + [] + ); const moveCursor = useCallback((x: number, y: number) => { if (!cursorRef.current) return; const { x: offsetX, y: offsetY } = getContainingBlockOffset(containingBlockRef.current); - gsap.to(cursorRef.current, { x: x - offsetX, y: y - offsetY, duration: 0.1, ease: 'power3.out' }); + gsap.to(cursorRef.current, { + x: x - offsetX, + y: y - offsetY, + duration: 0.1, + ease: 'power3.out' + }); }, []); useEffect(() => { @@ -124,19 +139,26 @@ const TargetCursor: React.FC = ({ if (!targetCornerPositionsRef.current || !cursorRef.current || !cornersRef.current) { return; } + const strength = activeStrengthRef.current.current; if (strength === 0) return; + const cursorX = gsap.getProperty(cursorRef.current, 'x') as number; const cursorY = gsap.getProperty(cursorRef.current, 'y') as number; + const corners = Array.from(cornersRef.current); corners.forEach((corner, i) => { const currentX = gsap.getProperty(corner, 'x') as number; const currentY = gsap.getProperty(corner, 'y') as number; + const targetX = targetCornerPositionsRef.current![i].x - cursorX; const targetY = targetCornerPositionsRef.current![i].y - cursorY; + const finalX = currentX + (targetX - currentX) * strength; const finalY = currentY + (targetY - currentY) * strength; + const duration = strength >= 0.99 ? (parallaxOn ? 0.2 : 0) : 0.05; + gsap.to(corner, { x: finalX, y: finalY, @@ -205,11 +227,27 @@ const TargetCursor: React.FC = ({ activeTarget = target; const corners = Array.from(cornersRef.current); - corners.forEach(corner => gsap.killTweensOf(corner)); + corners.forEach(corner => gsap.killTweensOf(corner, 'x,y')); + gsap.killTweensOf(cursorRef.current, 'rotation'); spinTl.current?.pause(); gsap.set(cursorRef.current, { rotation: 0 }); + if (cursorColorOnTarget) { + gsap.to(corners, { + borderColor: cursorColorOnTarget, + duration: 0.15, + ease: 'power2.out' + }); + if (dotRef.current) { + gsap.to(dotRef.current, { + backgroundColor: cursorColorOnTarget, + duration: 0.15, + ease: 'power2.out' + }); + } + } + const rect = target.getBoundingClientRect(); const { borderWidth, cornerSize } = constants; const { x: offsetX, y: offsetY } = getOffset(); @@ -226,7 +264,11 @@ const TargetCursor: React.FC = ({ isActiveRef.current = true; gsap.ticker.add(tickerFnRef.current!); - gsap.to(activeStrengthRef.current, { current: 1, duration: hoverDuration, ease: 'power2.out' }); + gsap.to(activeStrengthRef.current, { + current: 1, + duration: hoverDuration, + ease: 'power2.out' + }); corners.forEach((corner, i) => { gsap.to(corner, { @@ -239,13 +281,30 @@ const TargetCursor: React.FC = ({ const leaveHandler = () => { gsap.ticker.remove(tickerFnRef.current!); + isActiveRef.current = false; targetCornerPositionsRef.current = null; gsap.set(activeStrengthRef.current, { current: 0, overwrite: true }); activeTarget = null; + + if (cursorColorOnTarget && cornersRef.current) { + gsap.to(Array.from(cornersRef.current), { + borderColor: cursorColor, + duration: 0.15, + ease: 'power2.out' + }); + if (dotRef.current) { + gsap.to(dotRef.current, { + backgroundColor: cursorColor, + duration: 0.15, + ease: 'power2.out' + }); + } + } + if (cornersRef.current) { const corners = Array.from(cornersRef.current); - gsap.killTweensOf(corners); + gsap.killTweensOf(corners, 'x,y'); const { cornerSize } = constants; const positions = [ { x: -cornerSize * 1.5, y: -cornerSize * 1.5 }, @@ -255,9 +314,19 @@ const TargetCursor: React.FC = ({ ]; const tl = gsap.timeline(); corners.forEach((corner, index) => { - tl.to(corner, { x: positions[index].x, y: positions[index].y, duration: 0.3, ease: 'power3.out' }, 0); + tl.to( + corner, + { + x: positions[index].x, + y: positions[index].y, + duration: 0.3, + ease: 'power3.out' + }, + 0 + ); }); } + resumeTimeout = setTimeout(() => { if (!activeTarget && cursorRef.current && spinTl.current) { const currentRotation = gsap.getProperty(cursorRef.current, 'rotation') as number; @@ -277,13 +346,15 @@ const TargetCursor: React.FC = ({ } resumeTimeout = null; }, 50); + cleanupTarget(target); }; + currentLeaveHandler = leaveHandler; target.addEventListener('mouseleave', leaveHandler); }; - window.addEventListener('mouseover', enterHandler as EventListener); + window.addEventListener('mouseover', enterHandler as EventListener, { passive: true }); const resizeHandler = () => { containingBlockRef.current = getContainingBlock(cursor); @@ -294,22 +365,37 @@ const TargetCursor: React.FC = ({ if (tickerFnRef.current) { gsap.ticker.remove(tickerFnRef.current); } + window.removeEventListener('mousemove', moveHandler); window.removeEventListener('mouseover', enterHandler as EventListener); window.removeEventListener('scroll', scrollHandler); window.removeEventListener('resize', resizeHandler); window.removeEventListener('mousedown', mouseDownHandler); window.removeEventListener('mouseup', mouseUpHandler); + if (activeTarget) { cleanupTarget(activeTarget); } + spinTl.current?.kill(); document.body.style.cursor = originalCursor; + isActiveRef.current = false; targetCornerPositionsRef.current = null; activeStrengthRef.current.current = 0; }; - }, [targetSelector, spinDuration, moveCursor, constants, hideDefaultCursor, isMobile, hoverDuration, parallaxOn]); + }, [ + targetSelector, + spinDuration, + moveCursor, + constants, + hideDefaultCursor, + isMobile, + hoverDuration, + parallaxOn, + cursorColor, + cursorColorOnTarget + ]); useEffect(() => { if (isMobile || !cursorRef.current || !spinTl.current) return; @@ -327,13 +413,13 @@ const TargetCursor: React.FC = ({ return (
-
-
-
-
-
+
+
+
+
+
); }; -export default TargetCursor; +export default TargetCursor; \ No newline at end of file diff --git a/src/ts-tailwind/Animations/TargetCursor/TargetCursor.tsx b/src/ts-tailwind/Animations/TargetCursor/TargetCursor.tsx index f9a2c4461..f2363023c 100644 --- a/src/ts-tailwind/Animations/TargetCursor/TargetCursor.tsx +++ b/src/ts-tailwind/Animations/TargetCursor/TargetCursor.tsx @@ -37,6 +37,8 @@ export interface TargetCursorProps { hideDefaultCursor?: boolean; hoverDuration?: number; parallaxOn?: boolean; + cursorColor?: string; + cursorColorOnTarget?: string; } const TargetCursor: React.FC = ({ @@ -44,7 +46,9 @@ const TargetCursor: React.FC = ({ spinDuration = 2, hideDefaultCursor = true, hoverDuration = 0.2, - parallaxOn = true + parallaxOn = true, + cursorColor = '#ffffff', + cursorColorOnTarget }) => { const cursorRef = useRef(null); const cornersRef = useRef | null>(null); @@ -204,11 +208,26 @@ const TargetCursor: React.FC = ({ activeTarget = target; const corners = Array.from(cornersRef.current); - corners.forEach(corner => gsap.killTweensOf(corner)); + corners.forEach(corner => gsap.killTweensOf(corner, 'x,y')); gsap.killTweensOf(cursorRef.current, 'rotation'); spinTl.current?.pause(); gsap.set(cursorRef.current, { rotation: 0 }); + if (cursorColorOnTarget) { + gsap.to(corners, { + borderColor: cursorColorOnTarget, + duration: 0.15, + ease: 'power2.out' + }); + if (dotRef.current) { + gsap.to(dotRef.current, { + backgroundColor: cursorColorOnTarget, + duration: 0.15, + ease: 'power2.out' + }); + } + } + const rect = target.getBoundingClientRect(); const { borderWidth, cornerSize } = constants; const { x: offsetX, y: offsetY } = getOffset(); @@ -242,9 +261,25 @@ const TargetCursor: React.FC = ({ targetCornerPositionsRef.current = null; gsap.set(activeStrengthRef.current, { current: 0, overwrite: true }); activeTarget = null; + + if (cursorColorOnTarget && cornersRef.current) { + gsap.to(Array.from(cornersRef.current), { + borderColor: cursorColor, + duration: 0.15, + ease: 'power2.out' + }); + if (dotRef.current) { + gsap.to(dotRef.current, { + backgroundColor: cursorColor, + duration: 0.15, + ease: 'power2.out' + }); + } + } + if (cornersRef.current) { const corners = Array.from(cornersRef.current); - gsap.killTweensOf(corners); + gsap.killTweensOf(corners, 'x,y'); const { cornerSize } = constants; const positions = [ { x: -cornerSize * 1.5, y: -cornerSize * 1.5 }, @@ -308,7 +343,18 @@ const TargetCursor: React.FC = ({ targetCornerPositionsRef.current = null; activeStrengthRef.current.current = 0; }; - }, [targetSelector, spinDuration, moveCursor, constants, hideDefaultCursor, isMobile, hoverDuration, parallaxOn]); + }, [ + targetSelector, + spinDuration, + moveCursor, + constants, + hideDefaultCursor, + isMobile, + hoverDuration, + parallaxOn, + cursorColor, + cursorColorOnTarget + ]); useEffect(() => { if (isMobile || !cursorRef.current || !spinTl.current) return; @@ -332,27 +378,27 @@ const TargetCursor: React.FC = ({ >
); }; -export default TargetCursor; +export default TargetCursor; \ No newline at end of file From 9c86c31c8198b4d90d53b1fe2637a8d6d8565e3c Mon Sep 17 00:00:00 2001 From: Caleb Date: Sat, 20 Jun 2026 21:09:35 +0000 Subject: [PATCH 2/2] feat(TargetCursorDemo): wire up cursorColor and cursorColorOnTarget controls --- src/demo/Animations/TargetCursorDemo.jsx | 33 +++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/demo/Animations/TargetCursorDemo.jsx b/src/demo/Animations/TargetCursorDemo.jsx index 6c54b800c..835ba4901 100644 --- a/src/demo/Animations/TargetCursorDemo.jsx +++ b/src/demo/Animations/TargetCursorDemo.jsx @@ -11,6 +11,7 @@ import PropTable from '../../components/common/Preview/PropTable'; import Dependencies from '../../components/code/Dependencies'; import PreviewSlider from '../../components/common/Preview/PreviewSlider'; import PreviewSwitch from '../../components/common/Preview/PreviewSwitch'; +import PreviewColorPickerCustom from '../../components/common/Preview/PreviewColorPickerCustom'; import TargetCursor from '../../content/Animations/TargetCursor/TargetCursor'; import { targetCursor } from '../../constants/code/Animations/targetCursorCode'; @@ -19,12 +20,14 @@ const DEFAULT_PROPS = { spinDuration: 2, hideDefaultCursor: true, hoverDuration: 0.2, - parallaxOn: true + parallaxOn: true, + cursorColor: '#ffffff', + cursorColorOnTarget: '#B497CF' }; const TargetCursorDemo = () => { const { props, updateProp, resetProps, hasChanges } = useComponentProps(DEFAULT_PROPS); - const { spinDuration, hideDefaultCursor, hoverDuration, parallaxOn } = props; + const { spinDuration, hideDefaultCursor, hoverDuration, parallaxOn, cursorColor, cursorColorOnTarget } = props; const propData = useMemo( () => [ @@ -57,6 +60,18 @@ const TargetCursorDemo = () => { type: 'boolean', default: 'true', description: 'Enables a subtle parallax effect on the corners when moving over a target' + }, + { + name: 'cursorColor', + type: 'string', + default: "'#ffffff'", + description: 'Color of the cursor dot and corner brackets at rest' + }, + { + name: 'cursorColorOnTarget', + type: 'string', + default: 'undefined', + description: 'Optional color the cursor smoothly transitions to while locked onto a target' } ], [] @@ -172,6 +187,16 @@ const TargetCursorDemo = () => { isChecked={parallaxOn} onChange={val => updateProp('parallaxOn', val)} /> + updateProp('cursorColor', val)} + /> + updateProp('cursorColorOnTarget', val)} + /> @@ -189,9 +214,11 @@ const TargetCursorDemo = () => { hideDefaultCursor={hideDefaultCursor} hoverDuration={hoverDuration} parallaxOn={parallaxOn} + cursorColor={cursorColor} + cursorColorOnTarget={cursorColorOnTarget} /> ); }; -export default TargetCursorDemo; +export default TargetCursorDemo; \ No newline at end of file