Files
jaengseung-made/components/ui/3d-card-effect.tsx
gahusb 7ee75f1511 feat(ui): Liquid Glass + Aceternity 컴포넌트 도입 (clsx·framer-motion·tailwind-merge)
- LiquidGlass: GlassButton·GlassFilter (Apple Liquid Glass 효과)
- 3d-card-effect: 마우스 추적 3D 카드 래퍼
- sparkles-text: SparklesText·SparklesOverlay
- lib/utils.ts: cn() (clsx + tailwind-merge)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 02:12:21 +09:00

140 lines
3.6 KiB
TypeScript

'use client';
import { cn } from '@/lib/utils';
import React, {
createContext,
useState,
useContext,
useRef,
useEffect,
} from 'react';
const MouseEnterContext = createContext<
[boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined
>(undefined);
export const CardContainer = ({
children,
className,
containerClassName,
}: {
children?: React.ReactNode;
className?: string;
containerClassName?: string;
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isMouseEntered, setIsMouseEntered] = useState(false);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const { left, top, width, height } = containerRef.current.getBoundingClientRect();
const x = (e.clientX - left - width / 2) / 25;
const y = (e.clientY - top - height / 2) / 25;
containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`;
};
const handleMouseEnter = () => {
setIsMouseEntered(true);
};
const handleMouseLeave = () => {
if (!containerRef.current) return;
setIsMouseEntered(false);
containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`;
};
return (
<MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>
<div
className={cn('flex items-center justify-center', containerClassName)}
style={{ perspective: '1000px' }}
>
<div
ref={containerRef}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className={cn(
'flex items-center justify-center relative transition-all duration-200 ease-linear w-full',
className,
)}
style={{ transformStyle: 'preserve-3d' }}
>
{children}
</div>
</div>
</MouseEnterContext.Provider>
);
};
export const CardBody = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div
className={cn(
'[transform-style:preserve-3d] [&>*]:[transform-style:preserve-3d]',
className,
)}
>
{children}
</div>
);
};
type CardItemProps = {
as?: React.ElementType;
children: React.ReactNode;
className?: string;
translateX?: number | string;
translateY?: number | string;
translateZ?: number | string;
rotateX?: number | string;
rotateY?: number | string;
rotateZ?: number | string;
} & Record<string, unknown>;
export const CardItem = ({
as: Tag = 'div',
children,
className,
translateX = 0,
translateY = 0,
translateZ = 0,
rotateX = 0,
rotateY = 0,
rotateZ = 0,
...rest
}: CardItemProps) => {
const ref = useRef<HTMLDivElement>(null);
const [isMouseEntered] = useMouseEnter();
useEffect(() => {
if (!ref.current) return;
if (isMouseEntered) {
ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`;
} else {
ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`;
}
}, [isMouseEntered, translateX, translateY, translateZ, rotateX, rotateY, rotateZ]);
return (
<Tag ref={ref} className={cn('transition duration-200 ease-linear', className)} {...rest}>
{children}
</Tag>
);
};
export const useMouseEnter = () => {
const context = useContext(MouseEnterContext);
if (context === undefined) {
throw new Error('useMouseEnter must be used within a MouseEnterProvider');
}
return context;
};