Bend Hover Line

Hover
SVG
JS
Last Updated: Mar 3, 2026
<script>
document.addEventListener("DOMContentLoaded", function () {
	const useCSSPath = CSS.supports('d', 'path("M0,0")');

	document.querySelectorAll(".hover-7_wrap").forEach((component) => {
		if (component.hasAttribute("data-hover-7")) return;
		component.setAttribute("data-hover-7", "");
		const path = component.querySelector("path");
		let targetY = 80;
		let currentY = 80;
		let velocityY = 0;
		let targetX = 500;
		let currentX = 500;
		let isHovering = false;
		let animating = false;
		let releaseDirection = 0;
		let lastClientX = 0;
		let lastClientY = 0;
		let touchActive = false;
		const MID_Y = 80;
		const LERP_SPEED = 0.21;

		function updatePath() {
			const cx = Math.round(currentX * 10) / 10;
			const cy = Math.round(currentY * 10) / 10;
			const d = `M0,80 Q${cx},${cy} 1000,80`;
			if (useCSSPath) {
				path.style.d = `path("${d}")`;
			} else {
				path.setAttribute("d", d);
			}
		}

		function animate() {
			if (isHovering) {
				currentX += (targetX - currentX) * LERP_SPEED;
				currentY += (targetY - currentY) * LERP_SPEED;
				velocityY = currentY - MID_Y;
			} else {
				const displacement = currentY - MID_Y;
				const crossed = releaseDirection !== 0 && Math.sign(displacement) !== releaseDirection;
				const springForce = -0.028 * displacement;
				velocityY += springForce;
				velocityY *= crossed ? 0.83 : 0.965;
				currentY += velocityY;
				currentX += (500 - currentX) * 0.056;
			}
			updatePath();
			const settled = !isHovering && Math.abs(currentY - MID_Y) < 0.05 && Math.abs(velocityY) < 0.05 && Math.abs(currentX - 500) < 0.5;
			if (settled) {
				currentY = MID_Y;
				currentX = 500;
				velocityY = 0;
				updatePath();
				animating = false;
				return;
			}
			animating = true;
			requestAnimationFrame(animate);
		}

		function startAnim() {
			if (!animating) {
				animating = true;
				animate();
			}
		}

		function hitTest(clientX, clientY) {
			const rect = component.getBoundingClientRect();
			const relX = (clientX - rect.left) / rect.width;
			const relY = (clientY - rect.top) / rect.height;
			if (relX >= 0 && relX <= 1 && relY >= 0 && relY <= 1) {
				targetX = relX * 1000;
				targetY = relY * 240 - 40;
				isHovering = true;
				startAnim();
			} else if (isHovering) {
				isHovering = false;
				releaseDirection = Math.sign(currentY - MID_Y);
				velocityY = 0;
				startAnim();
			}
		}

		function releaseAll() {
			isHovering = false;
			touchActive = false;
			if (Math.abs(currentY - MID_Y) > 0.05 || Math.abs(velocityY) > 0.05) {
				releaseDirection = Math.sign(currentY - MID_Y);
				velocityY = 0;
				startAnim();
			}
		}

		let isTouchDevice = false;
		document.addEventListener("touchstart", () => { isTouchDevice = true; }, { once: true, passive: true });

		document.addEventListener("mousemove", (e) => {
			lastClientX = e.clientX;
			lastClientY = e.clientY;
			hitTest(e.clientX, e.clientY);
		});

		document.addEventListener("mouseleave", () => { releaseAll(); });

		document.addEventListener("touchstart", (e) => {
			const touch = e.touches[0];
			if (touch) {
				touchActive = true;
				lastClientX = touch.clientX;
				lastClientY = touch.clientY;
				hitTest(touch.clientX, touch.clientY);
			}
		}, { passive: true });

		document.addEventListener("touchmove", (e) => {
			const touch = e.touches[0];
			if (touch) {
				lastClientX = touch.clientX;
				lastClientY = touch.clientY;
				if (touchActive) hitTest(touch.clientX, touch.clientY);
			}
		}, { passive: true });

		document.addEventListener("touchend", () => { releaseAll(); }, { passive: true });
		document.addEventListener("touchcancel", () => { releaseAll(); }, { passive: true });

		let scrollTicking = false;
		window.addEventListener("scroll", () => {
			if (!isTouchDevice && (lastClientX || lastClientY)) {
				if (!scrollTicking) {
					scrollTicking = true;
					requestAnimationFrame(() => {
						hitTest(lastClientX, lastClientY);
						scrollTicking = false;
					});
				}
			}
		}, { passive: true });
	});
});
</script>