diff --git a/frontend/src/components/Animation/AnimatedCard.tsx b/frontend/src/components/Animation/AnimatedCard.tsx new file mode 100644 index 0000000..dad0cc1 --- /dev/null +++ b/frontend/src/components/Animation/AnimatedCard.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from "react"; +import AnimationWrapper from "./AnimationWrapper"; + +interface AnimatedCardProps { + children: ReactNode; + delay?: number; + className?: string; +} + +export default function AnimatedCard(props: AnimatedCardProps) { + return ; +} \ No newline at end of file diff --git a/frontend/src/components/Animation/AnimationWrapper.tsx b/frontend/src/components/Animation/AnimationWrapper.tsx new file mode 100644 index 0000000..e0a9529 --- /dev/null +++ b/frontend/src/components/Animation/AnimationWrapper.tsx @@ -0,0 +1,57 @@ +import type { ReactNode } from "react"; +import { motion } from "framer-motion"; + +import { fadeIn, slideUp, slideLeft, scaleIn } from "@/utils/animationConfig"; +import { useInViewAnimation } from "@/hooks/useInViewAnimation"; + +type AnimationType = "fade" | "slideUp" | "slideLeft" | "scale"; + +interface AnimationWrapperProps { + children: ReactNode; + type?: AnimationType; + delay?: number; + duration?: number; + className?: string; + once?: boolean; +} + +const variantsMap = { + fade: fadeIn, + slideUp: slideUp, + slideLeft: slideLeft, + scale: scaleIn, +}; + +export default function AnimationWrapper({ + children, + type = "fade", + delay = 0, + duration, + className, + once = true, +}: AnimationWrapperProps) { + const { ref, isInView, prefersReducedMotion } = + useInViewAnimation({ once }); + + if (prefersReducedMotion) { + return
{children}
; + } + + const variants = variantsMap[type]; + + return ( + } + className={className} + variants={variants} + initial="hidden" + animate={isInView ? "visible" : "hidden"} + transition={{ + delay, + ...(duration ? { duration } : {}), + }} + > + {children} + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Animation/FadeIn.tsx b/frontend/src/components/Animation/FadeIn.tsx new file mode 100644 index 0000000..7f5dbec --- /dev/null +++ b/frontend/src/components/Animation/FadeIn.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; +import AnimationWrapper from "./AnimationWrapper"; + +interface FadeInProps { + children: ReactNode; + delay?: number; + duration?: number; + className?: string; +} + +export default function FadeIn(props: FadeInProps) { + return ; +} \ No newline at end of file diff --git a/frontend/src/components/Animation/SlideIn.tsx b/frontend/src/components/Animation/SlideIn.tsx new file mode 100644 index 0000000..b706ebf --- /dev/null +++ b/frontend/src/components/Animation/SlideIn.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; +import AnimationWrapper from "./AnimationWrapper"; + +interface SlideInProps { + children: ReactNode; + delay?: number; + duration?: number; + className?: string; +} + +export default function SlideIn(props: SlideInProps) { + return ; +} \ No newline at end of file diff --git a/frontend/src/hooks/useInViewAnimation.ts b/frontend/src/hooks/useInViewAnimation.ts new file mode 100644 index 0000000..32e17c3 --- /dev/null +++ b/frontend/src/hooks/useInViewAnimation.ts @@ -0,0 +1,28 @@ +import { useRef } from "react"; +import { useInView, useReducedMotion } from "framer-motion"; + +interface UseInViewAnimationOptions { + once?: boolean; + amount?: "some" | "all" | number; +} + +export const useInViewAnimation = ( + options: UseInViewAnimationOptions = {} +) => { + const { once = true, amount = 0.2 } = options; + + const ref = useRef(null); + + const isInView = useInView(ref, { + once, + amount, + }); + + const prefersReducedMotion = useReducedMotion(); + + return { + ref, + isInView, + prefersReducedMotion, + }; +}; \ No newline at end of file diff --git a/frontend/src/utils/animationConfig.ts b/frontend/src/utils/animationConfig.ts new file mode 100644 index 0000000..ecb8245 --- /dev/null +++ b/frontend/src/utils/animationConfig.ts @@ -0,0 +1,58 @@ +import type { Variants } from "framer-motion"; +export const fadeIn: Variants = { + hidden: { + opacity: 0, + }, + visible: { + opacity: 1, + transition: { + duration: 0.5, + ease: "easeOut", + }, + }, +}; + +export const slideUp: Variants = { + hidden: { + opacity: 0, + y: 20, + }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.5, + ease: "easeOut", + }, + }, +}; + +export const slideLeft: Variants = { + hidden: { + opacity: 0, + x: 20, + }, + visible: { + opacity: 1, + x: 0, + transition: { + duration: 0.5, + ease: "easeOut", + }, + }, +}; + +export const scaleIn: Variants = { + hidden: { + opacity: 0, + scale: 0.95, + }, + visible: { + opacity: 1, + scale: 1, + transition: { + duration: 0.45, + ease: "easeOut", + }, + }, +}; \ No newline at end of file