Initial commit

This commit is contained in:
dk
2025-12-19 19:02:32 +02:00
commit a667749a63
308 changed files with 64886 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
"use client";
import { memo } from "react";
import useElasticEffect from "./useElasticEffect";
import { useButtonClick } from "../useButtonClick";
import { cls } from "@/lib/utils";
interface ButtonElasticEffectProps {
text: string;
onClick?: () => void;
href?: string;
className?: string;
textClassName?: string;
disabled?: boolean;
ariaLabel?: string;
type?: "button" | "submit" | "reset";
}
const ButtonElasticEffect = ({
text,
onClick,
href,
className = "",
textClassName = "",
disabled = false,
ariaLabel,
type = "button",
}: ButtonElasticEffectProps) => {
const elasticRef = useElasticEffect<HTMLButtonElement>();
const handleClick = useButtonClick(href, onClick);
return (
<button
ref={elasticRef}
type={type}
onClick={handleClick}
disabled={disabled}
aria-label={ariaLabel || text}
data-href={href}
className={cls(
"relative cursor-pointer h-9 min-w-0 w-fit max-w-full px-6 primary-button rounded-theme text-background",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
>
<span className={cls("text-sm block overflow-hidden truncate whitespace-nowrap", textClassName)}>{text}</span>
</button>
);
};
ButtonElasticEffect.displayName = "ButtonElasticEffect";
export default memo(ButtonElasticEffect);

View File

@@ -0,0 +1,59 @@
"use client";
import { useRef, useEffect, useCallback } from "react";
import gsap from "gsap";
const useElasticEffect = <T extends HTMLElement>() => {
const elementRef = useRef<T>(null);
const hoverLockedRef = useRef(false);
const timelineRef = useRef<gsap.core.Timeline | null>(null);
const handleMouseEnter = useCallback(() => {
const el = elementRef.current;
if (!el || hoverLockedRef.current) return;
hoverLockedRef.current = true;
setTimeout(() => {
hoverLockedRef.current = false;
}, 500);
const w = el.offsetWidth;
const h = el.offsetHeight;
const fs = parseFloat(getComputedStyle(el).fontSize);
const stretch = 0.75 * fs;
const sx = (w + stretch) / w;
const sy = (h - stretch * 0.33) / h;
if (timelineRef.current) {
timelineRef.current.kill();
}
timelineRef.current = gsap
.timeline()
.to(el, { scaleX: sx, scaleY: sy, duration: 0.1, ease: "power1.out" })
.to(el, { scaleX: 1, scaleY: 1, duration: 1, ease: "elastic.out(1, 0.3)" });
}, []);
useEffect(() => {
// Skip on touch devices
if (window.matchMedia("(hover: none) and (pointer: coarse)").matches) {
return;
}
const el = elementRef.current;
if (!el) return;
el.addEventListener("mouseenter", handleMouseEnter);
return () => {
el.removeEventListener("mouseenter", handleMouseEnter);
if (timelineRef.current) {
timelineRef.current.kill();
}
};
}, [handleMouseEnter]);
return elementRef;
};
export default useElasticEffect;