/** * StyleSeed — Motion Primitives (Framer Motion) * * Pre-configured animation wrappers that follow the design language. * Install: npm install framer-motion */ import * as React from "react" import { motion, AnimatePresence, type Variants } from "framer-motion" import { cn } from "./utils" // ── Duration & Easing Tokens ──────────────────────── export const duration = { fast: 0.1, normal: 0.2, moderate: 0.3, slow: 0.35, } as const export const ease = { default: [0.25, 0.1, 0.25, 1.0], in: [0.4, 0, 1, 1], out: [0, 0, 0.2, 1], spring: { type: "spring" as const, damping: 25, stiffness: 300 }, } as const // ── Preset Variants ───────────────────────────────── export const fadeIn: Variants = { hidden: { opacity: 0 }, visible: { opacity: 1 }, exit: { opacity: 0 }, } export const fadeUp: Variants = { hidden: { opacity: 0, y: 12 }, visible: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 12 }, } export const slideUp: Variants = { hidden: { opacity: 0, y: "100%" }, visible: { opacity: 1, y: 0 }, exit: { opacity: 0, y: "100%" }, } export const slideRight: Variants = { hidden: { x: "100%" }, visible: { x: 0 }, exit: { x: "100%" }, } export const scaleIn: Variants = { hidden: { opacity: 0, scale: 0.95 }, visible: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.95 }, } // ── FadeIn ────────────────────────────────────────── interface FadeInProps extends React.ComponentProps { delay?: number } function FadeIn({ delay = 0, className, children, ...props }: FadeInProps) { return ( {children} ) } // ── FadeUp (Card entry animation) ─────────────────── interface FadeUpProps extends React.ComponentProps { delay?: number } function FadeUp({ delay = 0, className, children, ...props }: FadeUpProps) { return ( {children} ) } // ── Stagger (Card grid entry) ─────────────────────── interface StaggerProps extends React.ComponentProps { staggerDelay?: number } function Stagger({ staggerDelay = 0.05, className, children, ...props }: StaggerProps) { return ( {children} ) } function StaggerItem({ className, children, ...props }: React.ComponentProps) { return ( {children} ) } // ── PresenceModal (Modal/Sheet enter + exit) ──────── interface PresenceModalProps { isOpen: boolean children: React.ReactNode variant?: "fade" | "slideUp" | "scale" className?: string } function PresenceModal({ isOpen, children, variant = "fade", className }: PresenceModalProps) { const variants = { fade: fadeIn, slideUp: slideUp, scale: scaleIn, } return ( {isOpen && ( {children} )} ) } // ── Backdrop ──────────────────────────────────────── interface BackdropProps { isOpen: boolean onClick?: () => void className?: string } function Backdrop({ isOpen, onClick, className }: BackdropProps) { return ( {isOpen && ( )} ) } // ── PageTransition (Page entry stagger) ───────────── function PageTransition({ className, children, ...props }: React.ComponentProps) { return ( {children} ) } function PageSection({ className, children, ...props }: React.ComponentProps) { return ( {children} ) } // ── NumberCounter (Hero metric counting) ──────────── interface NumberCounterProps { value: number duration?: number className?: string } function NumberCounter({ value, duration: d = 0.6, className }: NumberCounterProps) { const [display, setDisplay] = React.useState(0) const prefersReducedMotion = React.useRef( typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches ) React.useEffect(() => { if (prefersReducedMotion.current) { setDisplay(value) return } let start = 0 const startTime = performance.now() function step(currentTime: number) { const elapsed = (currentTime - startTime) / 1000 const progress = Math.min(elapsed / d, 1) const eased = 1 - Math.pow(1 - progress, 3) // ease-out cubic setDisplay(Math.round(eased * value)) if (progress < 1) requestAnimationFrame(step) } requestAnimationFrame(step) }, [value, d]) return {display.toLocaleString()} } export { FadeIn, FadeUp, Stagger, StaggerItem, PresenceModal, Backdrop, PageTransition, PageSection, NumberCounter, }