→ פרק קודם: ScrollTrigger — קסם שמונע מגלילה | פרק הבא: Lenis — גלילה חלקה ←

פרק 6: Motion (Framer Motion) — שכבת האנימציה של React

בפרקים 4-5 למדתם GSAP — מנוע אנימציה עוצמתי שעובד בכל מקום. אבל אם אתם עובדים ב-React (וב-2026 רוב ה-Vibe Coders עובדים ב-React דרך Next.js, Remix, או Vite), יש ספרייה שתוכננה בדיוק בשבילכם: Motion — שנולדה כ-Framer Motion ומ-2024 הפכה לספרייה עצמאית שתומכת ב-React, Vue, וגם vanilla JavaScript. Motion לא מחליפה את GSAP — היא ממלאת תפקיד אחר. היא שכבת אנימציה declarative שמשתלבת עם React כמו שרכיבים משתלבים — עם props, state, ו-lifecycle. במקום לכתוב gsap.to() בתוך useEffect ולדאוג ל-cleanup, כותבים <motion.div animate={{ opacity: 1 }}> וזה עובד. בפרק הזה תלמדו הכל: מ-motion.div בסיסי, דרך spring physics, variants, AnimatePresence, layout animations, gestures, scroll hooks — ועד טבלת ההחלטה GSAP vs Motion שתעזור לכם לבחור את הכלי הנכון.

מה תקבלו בסוף הפרק
מה תוכלו לעשות אחרי הפרק
לפני שמתחילים
הפרויקט שלך

בפרקים 4-5 בניתם hero section עם GSAP + ScrollTrigger — אנימציות imperative שאתם שולטים בכל שורה. בפרק הזה תבנו את אותו hero section מחדש ב-React עם Motion — ותראו את ההבדל הדרמטי בגישה. במקום gsap.from() ב-useEffect תכתבו <motion.h1 initial={{ opacity: 0 }} animate={{ opacity: 1 }}> ישירות ב-JSX. תוסיפו AnimatePresence לדפים שמשתנים, layout animations לכרטיסים שזזים, ו-useScroll לאפקטי scroll — הכל ב-React. בפרק 7 (Lenis) תלמדו לשלב smooth scroll עם Motion ליצירת חוויה שלמה.

מילון מונחים
מונח (English)תרגוםהגדרה
motion componentרכיב תנועהרכיב Motion שמחליף אלמנט HTML רגיל — motion.div, motion.span, motion.button וכו'. מקבל props של אנימציה כמו animate, initial, exit
animateמצב יעדה-prop הראשי של motion component — מגדיר את מצב היעד של האנימציה. למשל animate={{ opacity: 1, y: 0 }} מאנים לשקיפות מלאה ומיקום אפס
initialמצב התחלתימגדיר את המצב ההתחלתי לפני האנימציה. initial={{ opacity: 0, y: 20 }} = האלמנט מתחיל שקוף ו-20px למטה
exitמצב יציאהמגדיר את האנימציה שמתרחשת כשהרכיב יוצא מה-DOM. דורש AnimatePresence כ-wrapper. מה ש-CSS ו-React לא יכולים לעשות לבד
variantsוריאנטיםאובייקט שמגדיר animation states בשם — למשל { hidden: { opacity: 0 }, visible: { opacity: 1 } }. מאפשר הפעלה לפי שם, propagation לילדים, ו-orchestration
AnimatePresenceנוכחות מונפשתרכיב wrapper שמאפשר exit animations — כשרכיב ילד נמחק מה-DOM, AnimatePresence מעכב את המחיקה עד שאנימציית ה-exit מסתיימת
layoutIdמזהה פריסהprop שמקשר בין שני motion components — כשרכיב עם layoutId מסוים נעלם ורכיב אחר עם אותו layoutId מופיע, Motion מאנים ביניהם אוטומטית. "magic move"
springקפיץסוג transition שמדמה פיזיקה של קפיץ — stiffness (קשיחות), damping (ריסון), mass (מסה). ברירת המחדל ב-Motion. יוצר תנועה טבעית עם overshoot
useScrollהוק גלילהReact hook של Motion שמחזיר MotionValues של מיקום הגלילה — scrollY, scrollYProgress (0-1). משמש לאנימציות scroll ב-React
useTransformהוק טרנספורםReact hook שממפה MotionValue לטווח אחר — למשל ממפה scrollYProgress (0-1) ל-opacity (1-0). מחבר בין scroll לאנימציה בלי re-render
gestureמחוותאינטראקציה של המשתמש — whileHover, whileTap, whileDrag, whileFocus. ב-Motion, כל gesture הוא prop ישיר על motion component
orchestrationתזמורשליטה בסדר האנימציות של ילדים — staggerChildren (עיכוב בין ילדים), delayChildren (עיכוב כללי), when: "beforeChildren"/"afterChildren" (סדר הפעלה)
מתחיל 8 דקות מושג

6.1 מה זה Motion — מ-Framer Motion ל-Motion, ולמה זה משנה

Motion (שהייתה ידועה כ-Framer Motion עד 2024) היא ספריית אנימציה declarative שנולדה בתוך Framer — כלי עיצוב פרוטוטיפינג. מאט פרי (Matt Perry), המפתח הראשי, בנה אותה כדי לפתור בעיה ספציפית: אנימציות ב-React הן מסובכות. React הוא declarative — אתם מתארים מה אתם רוצים ו-React דואג ל-איך. אבל אנימציות הן inherently imperative — "הזז את האלמנט הזה מכאן לשם". הפער הזה יוצר קוד מכוער: useEffect עם refs, cleanup functions, התנגשויות עם re-renders. Motion פותרת את זה על ידי כך שהיא הופכת אנימציות ל-declarative — כמו React עצמה.

מ-Framer Motion ל-Motion: בסוף 2024, הספרייה עברה rebranding משמעותי. היא יצאה מתחת לכנפי Framer והפכה לפרויקט עצמאי בשם "Motion". השינוי לא היה רק בשם — Motion הרחיבה תמיכה ל-Vue וגם ל-vanilla JavaScript (בלי framework בכלל). ה-API נשאר כמעט זהה, אז כל מה שלמדתם על Framer Motion עדיין רלוונטי. הייבוא השתנה מ-"framer-motion" ל-"motion/react" (ל-React) או "motion" (vanilla). בקורס הזה נשתמש ב-"motion/react" — כי זה ה-import המודרני.

// הייבוא הישן (עדיין עובד אבל deprecated):
// import { motion } from "framer-motion";

// הייבוא המודרני (2024+):
import { motion } from "motion/react";

// ל-Vue:
// import { motion } from "motion/vue";

// ל-vanilla JavaScript (בלי framework):
// import { animate } from "motion";

למה Motion ולא CSS/GSAP ב-React? שלוש סיבות:

  1. Exit animations — כש-React מוחק רכיב מה-DOM, הוא פשוט נעלם. אין דרך נקייה לעשות "fade out before removal" ב-React רגיל. AnimatePresence של Motion פותרת את זה בשורה אחת. GSAP דורש ניהול ידני של ה-DOM lifecycle.
  2. Layout animations — כש-DOM משתנה (אלמנט עובר מיקום, רשימה מסתדרת מחדש), Motion מזהה את השינוי ומאנימת אוטומטית. בלי Motion, אתם צריכים לחשב positions ידנית (FLIP technique). Motion עושה את זה עם prop אחד: layout.
  3. React-native integration — אנימציות הן props, כמו style ו-className. אין צורך ב-useEffect, useRef, או cleanup. הקוד קריא ו-maintainable. AI כמו Bolt ו-Lovable מייצרים Motion code מצוין כי הוא declarative — קל לתאר בפרומפט.
3 דקות עשו עכשיו: התקנה ו-smoke test
  1. פתחו CodeSandbox או StackBlitz → תבנית React (Vite)
  2. התקינו: npm install motion
  3. ב-App.jsx, הוסיפו: import { motion } from "motion/react";
  4. החליפו div רגיל ב: <motion.div animate={{ scale: 1.2 }} transition={{ duration: 0.5 }}>Hello Motion</motion.div>
  5. אם ראיתם את הטקסט גדל — Motion עובד. מזל טוב, זו האנימציה הראשונה שלכם ב-React.
framer-motion vs motion — אל תתבלבלו בייבוא

אם אתם קוראים מדריכים ישנים (לפני 2024) או שה-AI מייצר קוד עם import { motion } from "framer-motion" — זה עדיין עובד, אבל ה-package הרשמי עכשיו הוא motion. אם אתם מתחילים פרויקט חדש, השתמשו ב-npm install motion ו-import מ-"motion/react". אם ה-AI מייצר את הייבוא הישן — פשוט שנו את שורת ה-import, ה-API זהה. שימו לב: ב-Bolt ו-Lovable, הפרומפט צריך לציין במפורש "use motion package (not framer-motion)" — אחרת ה-AI עלול להשתמש בגרסה הישנה.

מתחיל 12 דקות ליבה

6.2 motion component — הלב של Motion

הרעיון המרכזי של Motion הוא פשוט: במקום <div> רגיל, כתבו <motion.div>. זה אותו div בדיוק, אבל עם "על-כוחות" של אנימציה. כל אלמנט HTML יכול להפוך ל-motion component — motion.div, motion.span, motion.button, motion.h1, motion.img, motion.svg, motion.path ועוד. אפילו motion.li ו-motion.ul לרשימות.

שלושת ה-props הבסיסיים: initial, animate, exit

import { motion } from "motion/react";

// דוגמה 1: fade in פשוט
<motion.div
  initial={{ opacity: 0 }}      // מצב התחלתי — שקוף
  animate={{ opacity: 1 }}      // מצב יעד — נראה
>
  ברוכים הבאים
</motion.div>

// דוגמה 2: slide up + fade in
<motion.h1
  initial={{ opacity: 0, y: 30 }}    // מתחיל שקוף ו-30px למטה
  animate={{ opacity: 1, y: 0 }}     // מסתיים נראה ובמקום
  transition={{ duration: 0.6 }}     // 0.6 שניות
>
  כותרת ראשית
</motion.h1>

// דוגמה 3: כמה properties ביחד
<motion.div
  initial={{ opacity: 0, scale: 0.8, rotate: -10 }}
  animate={{ opacity: 1, scale: 1, rotate: 0 }}
  transition={{ duration: 0.5, ease: "easeOut" }}
>
  כרטיס מוצר
</motion.div>

מה קורה מאחורי הקלעים: כש-React מרנדר את ה-motion.div, Motion קורא את initial ומציב את האלמנט במצב ההתחלתי. אז היא מאנימת אוטומטית מ-initial ל-animate. אין צורך ב-useEffect או setTimeout — זה פשוט עובד. וכשה-props משתנים (למשל animate עובר מ-{ opacity: 1 } ל-{ opacity: 0 }), Motion מאנימת אוטומטית למצב החדש. זה ה-declarative magic — אתם מתארים את המצב, Motion דואגת לתנועה.

transition — שליטה בתזמון ובסוג האנימציה

// transition פשוט — duration ו-ease
<motion.div
  animate={{ x: 100 }}
  transition={{
    duration: 0.8,
    ease: "easeInOut"          // כמו CSS ease-in-out
  }}
/>

// transition עם delay
<motion.div
  animate={{ opacity: 1 }}
  transition={{
    duration: 0.5,
    delay: 0.3                 // מחכה 0.3 שניות לפני ההתחלה
  }}
/>

// transition שונה לכל property
<motion.div
  animate={{ opacity: 1, y: 0 }}
  transition={{
    opacity: { duration: 0.3 },           // opacity — 0.3 שניות
    y: { duration: 0.5, ease: "easeOut" } // y — 0.5 שניות, easeOut
  }}
/>

// repeat — אנימציה חוזרת
<motion.div
  animate={{ rotate: 360 }}
  transition={{
    duration: 2,
    repeat: Infinity,          // חוזר לנצח
    ease: "linear"             // מהירות קבועה לסיבוב
  }}
/>

easing ב-Motion: Motion תומך בכל ה-easing functions שלמדתם בפרק 3 — "easeIn", "easeOut", "easeInOut", "linear", ו-cubic bezier כמערך [0.42, 0, 0.58, 1]. אבל ברירת המחדל של Motion היא spring — לא ease! כשלא מציינים transition, Motion משתמשת ב-spring physics. זה אחד ההבדלים הגדולים מ-GSAP (ששם ברירת המחדל היא "power1.out"). בסעיף 6.3 נצלול לעומק ל-spring.

5 דקות עשו עכשיו: 3 אנימציות בסיסיות
  1. ב-CodeSandbox/StackBlitz, צרו 3 motion.div — כל אחד עם אנימציה שונה:
  2. div 1: fade in (opacity 0 → 1)
  3. div 2: slide up (y: 50 → 0) עם opacity
  4. div 3: scale in (scale: 0.5 → 1) עם rotate (rotate: -45 → 0)
  5. הוסיפו transition עם delay שונה לכל אחד (0, 0.2, 0.4) — כדי לראות stagger effect ידני
  6. נסו למחוק את transition לגמרי מאחד מהם — שימו לב שהוא עובר ל-spring (תנודה קלה בסוף)

animate דינמי — אנימציה שמגיבה ל-state

import { motion } from "motion/react";
import { useState } from "react";

function ToggleBox() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      <motion.div
        animate={{
          height: isOpen ? "auto" : 0,     // נפתח/נסגר
          opacity: isOpen ? 1 : 0
        }}
        transition={{ duration: 0.3 }}
        style={{ overflow: "hidden" }}
      >
        <p>תוכן שנפתח ונסגר בצורה חלקה</p>
      </motion.div>
    </div>
  );
}

// דוגמה 2: כפתור עם סטטוסים
function StatusButton() {
  const [status, setStatus] = useState("idle");

  const colors = {
    idle: "#3B82F6",        // כחול
    loading: "#F59E0B",     // צהוב
    success: "#10B981",     // ירוק
    error: "#EF4444"        // אדום
  };

  return (
    <motion.button
      animate={{
        backgroundColor: colors[status],
        scale: status === "loading" ? 0.95 : 1
      }}
      transition={{ duration: 0.3 }}
      whileTap={{ scale: 0.9 }}
      onClick={() => {
        setStatus("loading");
        setTimeout(() => setStatus("success"), 1500);
      }}
    >
      {status === "idle" && "שלח"}
      {status === "loading" && "שולח..."}
      {status === "success" && "נשלח! ✓"}
    </motion.button>
  );
}

זה הכוח האמיתי של Motion — אנימציות שמגיבות ל-state. שימו לב: אין useEffect, אין refs, אין cleanup. כשה-state משתנה, React מרנדר מחדש, Motion רואה שה-animate prop השתנה, ומאנימת אוטומטית. זה הרבה יותר נקי מהאלטרנטיבה עם GSAP:

// אותו דבר ב-GSAP — שימו לב לכמות הקוד:
import { useEffect, useRef } from "react";
import gsap from "gsap";

function ToggleBoxGSAP() {
  const [isOpen, setIsOpen] = useState(false);
  const boxRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      gsap.to(boxRef.current, { height: "auto", opacity: 1, duration: 0.3 });
    } else {
      gsap.to(boxRef.current, { height: 0, opacity: 0, duration: 0.3 });
    }
  }, [isOpen]);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      <div ref={boxRef} style={{ overflow: "hidden", opacity: 0, height: 0 }}>
        <p>תוכן שנפתח ונסגר</p>
      </div>
    </div>
  );
}
// יותר שורות, useEffect, ref, ותלות ב-isOpen בdependency array
5 דקות עשו עכשיו: toggle box עם Motion
  1. העתיקו את ToggleBox מלמעלה ל-CodeSandbox
  2. לחצו על הכפתור — ה-div צריך להיפתח ולהיסגר חלק
  3. שנו את ה-transition ל-type: "spring" (במקום duration) — שימו לב לאפקט הקפיצי
  4. הוסיפו initial={false} ל-motion.div — זה מדלג על אנימציית הכניסה הראשונה (שימושי כשהתוכן צריך להיות סגור בהתחלה בלי אנימציה)
בינוני 10 דקות פיזיקה

6.3 Spring Physics — תנועה שמרגישה אמיתית

בפרק 3 למדתם על easing — עקומות שמגדירות מהירות לאורך אנימציה. למדתם גם על spring physics — סימולציה של קפיץ פיזיקלי. בפרק הזה נצלול לעומק, כי spring הוא ברירת המחדל של Motion. כשכותבים animate={{ x: 100 }} בלי transition — Motion משתמשת ב-spring. זה אומר שהאלמנט ינוע כמו שמחובר לקפיץ: יתאץ, יעבור את היעד (overshoot), יתנדנד, ויתייצב. זה מרגיש טבעי כי זו פיזיקה אמיתית — חוקי ניוטון, לא עקומת Bezier שרירותית.

שלושת הפרמטרים: stiffness, damping, mass

// spring ברירת מחדל — מרגיש "חי" ותגובתי
<motion.div
  animate={{ x: 200 }}
  transition={{ type: "spring" }}     // ברירת מחדל — לא חייבים לציין
/>

// spring קשיח — תנועה מהירה, overshoot קטן
<motion.div
  animate={{ x: 200 }}
  transition={{
    type: "spring",
    stiffness: 300,    // קשיחות הקפיץ (ברירת מחדל: 100)
    damping: 20        // ריסון — כמה מהר התנודה נעצרת (ברירת מחדל: 10)
  }}
/>

// spring רך — תנועה איטית, תנודות רבות
<motion.div
  animate={{ x: 200 }}
  transition={{
    type: "spring",
    stiffness: 50,     // קפיץ רך
    damping: 5,        // ריסון נמוך = הרבה תנודות
    mass: 2            // מסה כפולה = תנועה כבדה יותר
  }}
/>

// spring "בוגר" — הגיע ליעד בלי overshoot
<motion.div
  animate={{ x: 200 }}
  transition={{
    type: "spring",
    stiffness: 100,
    damping: 20        // damping גבוה = בלי תנודה (critically damped)
  }}
/>
מדריך Spring Parameters
פרמטרברירת מחדלנמוךגבוהמשמעות בפרקטיקה
stiffness10050 = קפיץ רך, איטי500 = קפיץ קשיח, מהירכמה חזק הקפיץ "מושך" לכיוון היעד. UI buttons: 200-400. Page transitions: 80-150
damping105 = הרבה תנודות30 = כמעט בלי תנודהכמה מהר התנודה נעצרת. Playful: 8-12. Professional: 15-25. No bounce: 25+
mass10.5 = קל, תגובתי3 = כבד, עצלנימשפיע על ההתאצלות וזמן ההאטה. רוב הפעמים משאירים 1. גדול יותר = מרגיש "כבד"

קשר לפרק 3: בפרק 3 הסברנו שב-spring אין duration — האנימציה נמשכת עד שהקפיץ מתייצב. ב-Motion, אם מציינים duration ביחד עם type: "spring", Motion מתאימה אוטומטית את stiffness ו-damping כדי שהאנימציה תסתיים בזמן שציינתם. זה shortcut שימושי:

// spring עם duration — Motion מחשבת stiffness/damping אוטומטית
<motion.div
  animate={{ x: 200 }}
  transition={{
    type: "spring",
    duration: 0.5,     // האנימציה תסתיים בערך ב-0.5 שניות
    bounce: 0.3        // כמה "קפיצה" (0 = בלי, 1 = הרבה)
  }}
/>

// bounce — הדרך הקלה לשלוט ב-spring
// bounce: 0   = כמו ease — בלי overshoot
// bounce: 0.25 = קפיצה עדינה — מקצועי
// bounce: 0.5  = קפיצה בולטת — playful
// bounce: 0.8  = קפיצה מוגזמת — cartoon
5 דקות עשו עכשיו: הרגישו את ההבדל בין spring ל-ease
  1. צרו שני motion.div זה לצד זה, כל אחד מאנים x: 200 בלחיצת כפתור:
  2. div 1: transition={{ type: "spring", stiffness: 200, damping: 10 }} (spring)
  3. div 2: transition={{ duration: 0.5, ease: "easeOut" }} (ease)
  4. לחצו על הכפתור — שימו לב: ה-spring מתנדנד קצת בסוף, ה-ease עוצר "חלק". שניהם נכונים — אבל spring מרגיש "חי" יותר
  5. שנו את ה-damping ל-5 — עכשיו ה-spring מתנדנד הרבה יותר. העלו ל-25 — כמעט בלי תנודה

מתי spring ומתי ease?

Spring מתאים ל: כפתורים, כרטיסים, tooltips, dropdowns, modals — כל דבר אינטראקטיבי שהמשתמש מפעיל. ה-overshoot הקל מרגיש "חי" ותגובתי. Ease מתאים ל: page transitions, scroll animations, loading sequences, progress bars — תנועות ארוכות ומתוכננות שבהן overshoot יהיה מוזר. כלל אצבע: אם האנימציה מגיבה לאינטראקציה ישירה (click, hover, tap) — spring. אם היא חלק מ-sequence מתוכנן — ease.

spring + rotate = זהירות

Spring עם rotate יכול להרגיש מוזר — האלמנט "מתנדנד" ימינה-שמאלה כמו מטוטלת. לפעמים זה רצוי (אנימציה playful), אבל בדרך כלל עדיף ease לסיבובים. הפתרון: transition={{ rotate: { type: "tween", duration: 0.3, ease: "easeOut" }, default: { type: "spring" } }} — spring לכל ה-properties חוץ מ-rotate שמקבל ease.

בינוני 12 דקות ליבה

6.4 Variants — מצבי אנימציה בשם

עד עכשיו הגדרנו אנימציות inline — animate={{ opacity: 1 }}. זה עובד לאנימציות פשוטות, אבל כשיש כמה מצבים (hidden, visible, hovered, selected) ו-orchestration בין parent ל-children, צריך Variants. Variants הם אובייקט שמגדיר animation states לפי שם — כמו CSS classes, אבל לאנימציות.

import { motion } from "motion/react";

// הגדרת variants
const cardVariants = {
  hidden: {
    opacity: 0,
    y: 30
  },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.5, ease: "easeOut" }
  },
  hover: {
    y: -5,
    boxShadow: "0 10px 30px rgba(0,0,0,0.15)",
    transition: { type: "spring", stiffness: 300, damping: 20 }
  }
};

// שימוש ב-variants
<motion.div
  variants={cardVariants}
  initial="hidden"           // שם ה-variant ההתחלתי
  animate="visible"          // שם ה-variant היעד
  whileHover="hover"         // שם ה-variant בזמן hover
  className="card"
>
  תוכן הכרטיס
</motion.div>

למה variants ולא inline? שלוש סיבות: (1) קריאות — "hidden" ו-"visible" קריאים יותר מ-{{ opacity: 0, y: 30 }}. (2) Reusability — מגדירים פעם אחת, משתמשים בכמה רכיבים. (3) Propagation — parent יכול להפעיל variant על כל הילדים אוטומטית. וזה הכוח האמיתי:

Propagation — כש-parent שולט בילדים

// parent variants עם orchestration
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.15,     // 0.15 שניות בין כל ילד
      delayChildren: 0.3,        // 0.3 שניות לפני הילד הראשון
      when: "beforeChildren"     // ה-parent מסיים לפני הילדים
    }
  }
};

// children variants — לא צריכים transition, ה-parent שולט
const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 }
};

// שימוש — שימו לב: הילדים לא צריכים initial/animate!
function CardGrid({ items }) {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="grid"
    >
      {items.map((item) => (
        <motion.div
          key={item.id}
          variants={itemVariants}    // רק variants — בלי initial/animate
          className="card"
        >
          {item.title}
        </motion.div>
      ))}
    </motion.div>
  );
}

// התוצאה: container נכנס → 0.3 שניות המתנה → כרטיסים נכנסים
// אחד אחרי השני עם 0.15 שניות ביניהם. stagger אוטומטי!

Propagation עובד אוטומטית: כשה-parent עובר ל-variant "visible", כל ילד עם variants שמכיל "visible" יעבור גם הוא — אוטומטית, בלי שום קוד נוסף. זה עובד לכל עומק — ילדים של ילדים גם מקבלים את ה-variant. וה-orchestration (staggerChildren, delayChildren) מוגדר ב-parent ומשפיע על כל הילדים.

Orchestration — שליטה בסדר האנימציות

// staggerChildren — עיכוב בין ילד לילד
{
  visible: {
    transition: {
      staggerChildren: 0.1,          // 0.1s בין כל ילד
      staggerDirection: -1,          // הפוך — מהאחרון לראשון
    }
  }
}

// delayChildren — עיכוב לפני כל הילדים
{
  visible: {
    transition: {
      delayChildren: 0.5,            // חצי שנייה לפני הילד הראשון
      staggerChildren: 0.1
    }
  }
}

// when — סדר ה-parent מול הילדים
{
  visible: {
    opacity: 1,
    transition: {
      when: "beforeChildren",        // parent מסיים, אז ילדים מתחילים
      staggerChildren: 0.1
    }
  }
}
// אפשרויות when:
// "beforeChildren" — parent קודם, אז ילדים
// "afterChildren"  — ילדים קודם, אז parent
// לא מוגדר        — parent וילדים ביחד (ברירת מחדל)
7 דקות עשו עכשיו: stagger grid עם variants
  1. צרו grid של 6 כרטיסים (2×3) עם motion.div
  2. הגדירו containerVariants עם staggerChildren: 0.1 ו-delayChildren: 0.2
  3. הגדירו itemVariants עם hidden (opacity: 0, scale: 0.8) ו-visible (opacity: 1, scale: 1)
  4. שימו variants על ה-container, ורק variants (בלי initial/animate) על כל item
  5. צפו בתוצאה — הכרטיסים צריכים להיכנס אחד-אחד עם stagger
  6. שנו staggerDirection ל--1 — עכשיו הם נכנסים מהאחרון לראשון

Variants דינמיים — custom property

// variants שמקבלים פרמטר
const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: (index) => ({          // פונקציה שמקבלת custom
    opacity: 1,
    y: 0,
    transition: {
      delay: index * 0.1           // delay לפי index
    }
  })
};

// שימוש עם custom prop
{items.map((item, index) => (
  <motion.div
    key={item.id}
    variants={itemVariants}
    custom={index}                 // מעביר את ה-index כ-custom
    initial="hidden"
    animate="visible"
  />
))}

custom הוא prop שמעביר ערך דינמי ל-variant function. זה שימושי כש-staggerChildren לא מספיק — למשל כשרוצים delay שונה לפי מיקום בגריד (row ו-column), לא רק לפי סדר.

15 דקות תרגיל: Navigation menu עם variants
  1. בנו navigation menu עם 5 פריטים (motion.li בתוך motion.ul)
  2. הגדירו variants — hidden: הפריטים שקופים ו-30px משמאל. visible: נראים ובמקום
  3. ה-ul מקבל initial="hidden", animate מותנה ב-state (isMenuOpen)
  4. הוסיפו staggerChildren: 0.08 ל-container — כל פריט נכנס בזה אחרי זה
  5. הוסיפו כפתור Toggle שמחליף בין "hidden" ל-"visible"
  6. כשסוגרים — שנו staggerDirection ל--1 (הפריטים יוצאים מהאחרון לראשון)
  7. בונוס: הוסיפו whileHover variant לכל פריט — scale: 1.05 ו-x: 5

קריטריון הצלחה: התפריט נפתח עם stagger חלק, נסגר עם stagger הפוך, וכל פריט מגיב ל-hover.

בינוני 10 דקות ליבה

6.5 AnimatePresence — exit animations שלא אפשריות בלעדיו

בעיה מרכזית באנימציות React: כש-React מסיר רכיב מה-DOM (תנאי שמתקיים, רשימה שמתקצרת, דף שמשתנה), הרכיב פשוט נעלם. אין "exit animation". ב-CSS רגיל אין פתרון — :not(:defined) לא עובד. ב-GSAP צריך ניהול ידני (לעכב את ה-removal, להריץ אנימציה, ואז להסיר). AnimatePresence של Motion פותר את זה בצורה אלגנטית:

import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";

function Notification() {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>Toggle</button>

      <AnimatePresence>
        {isVisible && (
          <motion.div
            key="notification"           // key חיוני!
            initial={{ opacity: 0, y: -20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}  // ← exit animation!
            transition={{ duration: 0.3 }}
            className="notification"
          >
            הודעה חשובה!
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}
// כש-isVisible הופך ל-false, במקום שהרכיב נעלם מיידית —
// הוא מאנים ל-exit (fade out + slide up) ורק אז נמחק מה-DOM

איך זה עובד: AnimatePresence עוקב אחרי הילדים שלו (לפי key). כשילד "נעלם" (התנאי הופך ל-false), AnimatePresence לא מסיר אותו מיד — הוא מעכב את ההסרה, מפעיל את אנימציית ה-exit, ורק כשהאנימציה מסתיימת — מוחק מה-DOM. זה בדיוק מה שלא אפשרי בלעדיו.

AnimatePresence עם mode

// mode="wait" — מחכה שהישן ייצא לפני שהחדש נכנס
<AnimatePresence mode="wait">
  <motion.div
    key={currentPage}              // key שונה = רכיב שונה
    initial={{ opacity: 0, x: 50 }}
    animate={{ opacity: 1, x: 0 }}
    exit={{ opacity: 0, x: -50 }}
    transition={{ duration: 0.3 }}
  >
    {pages[currentPage]}
  </motion.div>
</AnimatePresence>

// mode="popLayout" — החדש נכנס מיד, הישן "נדחף" החוצה
<AnimatePresence mode="popLayout">
  ...
</AnimatePresence>

// mode="sync" (ברירת מחדל) — הישן יוצא והחדש נכנס ביחד
<AnimatePresence mode="sync">
  ...
</AnimatePresence>

mode="wait" הוא הכי נפוץ — במיוחד ל-page transitions. הדף הישן עושה fade-out, ורק אחרי שנעלם הדף החדש עושה fade-in. בלי mode="wait", שני הדפים יהיו על המסך בו-זמנית (הישן יוצא והחדש נכנס), מה שנראה לפעמים מוזר.

key חיוני ב-AnimatePresence

AnimatePresence מזהה שינויים על פי key. אם key לא משתנה — AnimatePresence לא יזהה שהרכיב "הוחלף". שתי טעויות נפוצות: (1) שוכחים key לגמרי — exit לא עובד. (2) משתמשים ב-index כ-key ברשימה — כשפריט נמחק מהאמצע, React מבלבל את ה-keys ו-AnimatePresence מאנים את הפריט הלא נכון. תמיד השתמשו ב-unique id כ-key — לא index.

AnimatePresence לרשימות

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "למד Motion" },
    { id: 2, text: "בנה פרויקט" },
    { id: 3, text: "deploy!" }
  ]);

  const removeTodo = (id) => {
    setTodos(todos.filter(t => t.id !== id));
  };

  return (
    <ul>
      <AnimatePresence>
        {todos.map(todo => (
          <motion.li
            key={todo.id}              // id ייחודי — לא index!
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: "auto" }}
            exit={{ opacity: 0, height: 0 }}
            transition={{ duration: 0.3 }}
            style={{ overflow: "hidden" }}
          >
            {todo.text}
            <button onClick={() => removeTodo(todo.id)}>×</button>
          </motion.li>
        ))}
      </AnimatePresence>
    </ul>
  );
}
// כשמוחקים todo, הוא לא נעלם מיד — הוא מתכווץ ומתפוגג בצורה חלקה
7 דקות עשו עכשיו: notification עם exit animation
  1. העתיקו את Notification מלמעלה ל-CodeSandbox
  2. לחצו Toggle — ה-notification צריך להופיע ולהיעלם בצורה חלקה
  3. שנו את ה-exit ל-{{ opacity: 0, scale: 0.5, rotate: 10 }} — עכשיו זה יותר דרמטי
  4. הוסיפו notification שני (עם key שונה) ו-mode="wait" — צפו שהראשון יוצא לפני שהשני נכנס
  5. נסו להסיר את ה-key — שימו לב ש-exit מפסיק לעבוד
מתקדם 12 דקות magic

6.6 Layout Animations — layoutId ו-Magic Move

Layout animations הן אולי הפיצ'ר הכי מרשים של Motion — ואחד שאין לו מקבילה קלה ב-GSAP או CSS. הרעיון: כש-DOM משתנה (אלמנט עובר מיקום, רשימה מסתדרת מחדש, כרטיס מתרחב), Motion מזהה את השינוי ומאנימת אוטומטית את המעבר. הטכניקה מאחורי הקלעים היא FLIP (First, Last, Invert, Play) — אבל Motion עושה את זה אוטומטית.

layout — אנימציה אוטומטית של שינויי פריסה

// כפתור toggle שמשנה justify-content
function LayoutToggle() {
  const [isRight, setIsRight] = useState(false);

  return (
    <div
      style={{
        display: "flex",
        justifyContent: isRight ? "flex-end" : "flex-start",
        padding: 20,
        background: "#eee",
        borderRadius: 12,
        cursor: "pointer"
      }}
      onClick={() => setIsRight(!isRight)}
    >
      <motion.div
        layout                          // ← רק המילה layout!
        style={{
          width: 60,
          height: 60,
          borderRadius: 30,
          background: "#3B82F6"
        }}
        transition={{ type: "spring", stiffness: 500, damping: 30 }}
      />
    </div>
  );
}
// כשלוחצים, ה-justify-content משתנה ← ה-ball צריך לזוז.
// בלי layout — הוא "קופץ" מיידית. עם layout — הוא מחליק בצורה חלקה!

layout prop אומר ל-Motion: "תעקוב אחרי המיקום שלי ב-DOM. אם הוא משתנה — תאנים אוטומטית". זה עובד לכל שינוי CSS — position, size, order, flex, grid. אין צורך לחשב coordinates ידנית. פשוט מוסיפים layout ו-Motion עושה את השאר.

layoutId — Magic Move בין רכיבים שונים

// Gallery עם expand — כרטיס "עף" למיקום חדש
function ImageGallery() {
  const [selectedId, setSelectedId] = useState(null);

  return (
    <div>
      {/* Grid של thumbnails */}
      <div className="grid">
        {images.map(img => (
          <motion.div
            key={img.id}
            layoutId={`card-${img.id}`}      // ← layoutId ייחודי
            onClick={() => setSelectedId(img.id)}
            className="thumbnail"
            style={{ cursor: "pointer" }}
          >
            <motion.img
              layoutId={`img-${img.id}`}     // layoutId גם לתמונה
              src={img.src}
            />
          </motion.div>
        ))}
      </div>

      {/* כרטיס מורחב */}
      <AnimatePresence>
        {selectedId && (
          <motion.div
            layoutId={`card-${selectedId}`}  // אותו layoutId!
            className="expanded-card"
            onClick={() => setSelectedId(null)}
          >
            <motion.img
              layoutId={`img-${selectedId}`}
              src={images.find(i => i.id === selectedId).src}
            />
            <motion.p
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            >
              תיאור מורחב של התמונה
            </motion.p>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}
// כשלוחצים על thumbnail — הכרטיס "עף" בצורה חלקה ממיקום הגריד
// למיקום ה-expanded. לחיצה שנייה — "עף" חזרה. Magic Move!

איך layoutId עובד: Motion מחפש שני רכיבים עם אותו layoutId. כשאחד נעלם ואחד מופיע (או שניהם קיימים אבל במיקומים שונים), Motion מחשב FLIP ומאנים ביניהם. זה עובד גם בין רכיבי React שונים לגמרי — כל עוד ל-layoutId אותו ערך. זו הסיבה שזה נקרא "magic move" — נראה כאילו אלמנט פיזי עובר ממקום למקום, גם אם ב-DOM אלה שני אלמנטים שונים.

20 דקות תרגיל: Tab system עם layout animations
  1. בנו tab component עם 3 tabs — כל tab מראה תוכן שונה
  2. הוסיפו underline indicator (קו תחתון) שנע בין ה-tabs עם layoutId="underline"
  3. כל tab button: motion.button עם whileHover={{ scale: 1.05 }}
  4. ה-underline: motion.div עם layoutId="underline" שמופיע רק מתחת ל-tab הפעיל
  5. התוכן: AnimatePresence עם mode="wait" — כשמחליפים tab, התוכן הישן יוצא והחדש נכנס
  6. transition ל-underline: type: "spring", stiffness: 300, damping: 25
  7. בונוס: הוסיפו layoutId גם לרקע של ה-tab הפעיל — כך שגם הרקע "עובר" בין tabs

קריטריון הצלחה: ה-underline "מחליק" בין tabs בצורה חלקה (לא קופץ), והתוכן מתחלף עם exit animation.

בינוני 10 דקות אינטראקציה

6.7 Gestures — whileHover, whileTap, whileDrag

אנימציות gesture הן אנימציות שקורות בתגובה ישירה לפעולת המשתמש — hover, tap (click), drag, focus. ב-CSS יש :hover ו-:active, אבל הם מוגבלים ל-CSS properties ואין שליטה ב-transition type (אין spring, אין orchestration). ב-Motion, gestures הם props ישירים על motion components — וכל עולם האנימציות זמין:

// whileHover — אנימציה בזמן hover
<motion.button
  whileHover={{
    scale: 1.05,
    boxShadow: "0 5px 15px rgba(0,0,0,0.2)"
  }}
  whileTap={{ scale: 0.95 }}        // בזמן לחיצה — "נדחס"
  transition={{ type: "spring", stiffness: 400, damping: 15 }}
>
  לחץ כאן
</motion.button>

// whileFocus — לאלמנטים focusable (input, button)
<motion.input
  whileFocus={{
    scale: 1.02,
    borderColor: "#3B82F6"
  }}
  transition={{ duration: 0.2 }}
  type="text"
  placeholder="הקלד כאן..."
/>

// שילוב — כרטיס אינטראקטיבי
<motion.div
  whileHover={{ y: -5, boxShadow: "0 10px 30px rgba(0,0,0,0.15)" }}
  whileTap={{ scale: 0.98 }}
  transition={{ type: "spring", stiffness: 300, damping: 20 }}
  className="card"
>
  <h3>כרטיס מוצר</h3>
  <p>תיאור המוצר</p>
</motion.div>

whileDrag — גרירה

// drag בסיסי — גרירה חופשית
<motion.div
  drag                                // מפעיל גרירה
  whileDrag={{ scale: 1.1, boxShadow: "0 15px 40px rgba(0,0,0,0.3)" }}
  dragConstraints={{ top: -100, bottom: 100, left: -100, right: 100 }}
  dragElastic={0.2}                   // כמה "נמתח" מעבר לגבולות
  dragTransition={{ bounceStiffness: 500, bounceDamping: 20 }}
  style={{ cursor: "grab", width: 100, height: 100, background: "#3B82F6", borderRadius: 12 }}
/>

// drag רק בציר אחד
<motion.div
  drag="x"                            // רק אופקית
  dragConstraints={{ left: 0, right: 300 }}
  whileDrag={{ scale: 1.05 }}
  style={{ width: 60, height: 60, borderRadius: 30, background: "#10B981" }}
/>

// drag עם snap back
<motion.div
  drag
  dragConstraints={{ top: 0, bottom: 0, left: 0, right: 0 }}  // snap back למקום
  dragElastic={0.5}                   // נמתח 50% ואז חוזר
  whileDrag={{ scale: 1.1 }}
/>

// drag עם dragSnapToOrigin
<motion.div
  drag
  dragSnapToOrigin                    // חוזר למקום המקורי אחרי שחרור
  whileDrag={{ scale: 1.2, rotate: 5 }}
/>

dragConstraints מגביל את טווח הגרירה. אפשר להעביר ערכים בפיקסלים (כמו למעלה) או ref לאלמנט אב — והגרירה תוגבל לגבולות האלמנט:

import { useRef } from "react";

function DragInBox() {
  const constraintsRef = useRef(null);

  return (
    <div ref={constraintsRef} style={{ width: 400, height: 300, background: "#f0f0f0" }}>
      <motion.div
        drag
        dragConstraints={constraintsRef}   // מוגבל לתוך ה-div האב
        whileDrag={{ scale: 1.1 }}
        style={{ width: 60, height: 60, borderRadius: 12, background: "#8B5CF6" }}
      />
    </div>
  );
}
5 דקות עשו עכשיו: כרטיס אינטראקטיבי עם 3 gestures
  1. צרו motion.div שמייצג כרטיס מוצר (200×250px, רקע לבן, צל קל)
  2. הוסיפו whileHover: { y: -8, boxShadow: "0 12px 30px rgba(0,0,0,0.15)" }
  3. הוסיפו whileTap: { scale: 0.97 }
  4. הוסיפו drag עם dragSnapToOrigin — כדי שאפשר לגרור את הכרטיס אבל הוא חוזר למקום
  5. שנו את ה-transition ל-spring עם stiffness: 300, damping: 20 — ראו איך ה-gestures מרגישים

Gesture events — callbacks

// events שמופעלים ב-gestures
<motion.div
  drag="x"
  onDragStart={(event, info) => console.log("התחיל גרירה", info.point)}
  onDrag={(event, info) => console.log("גורר", info.offset.x)}
  onDragEnd={(event, info) => {
    // info.velocity.x — מהירות הגרירה בסיום
    if (info.velocity.x > 500) {
      // swipe right — אפשר למשל למחוק
      console.log("Swipe right!");
    }
  }}
  onHoverStart={() => console.log("hover started")}
  onHoverEnd={() => console.log("hover ended")}
  onTap={(event, info) => console.log("tapped at", info.point)}
/>

ה-info object מכיל מידע שימושי: point (מיקום הנוכחי), offset (כמה זזנו מנקודת ההתחלה), velocity (מהירות). שימושי לבניית swipe gestures, dismiss actions, ו-drag-to-delete.

בינוני מתקדם 12 דקות scroll

6.8 useScroll + useTransform — scroll animations ב-React

בפרק 5 למדתם ScrollTrigger — הפתרון של GSAP לאנימציות scroll. Motion מציעה גישה אחרת עם שני hooks: useScroll (קורא את מיקום הגלילה) ו-useTransform (ממפה ערכים). בואו נראה את שניהם, ואז נשווה ל-ScrollTrigger.

useScroll — לקרוא את מיקום הגלילה

import { motion, useScroll, useTransform } from "motion/react";

function ScrollProgress() {
  // useScroll מחזיר MotionValues:
  const { scrollY, scrollYProgress } = useScroll();
  // scrollY — מיקום גלילה בפיקסלים (0, 100, 500...)
  // scrollYProgress — התקדמות 0-1 (0 = למעלה, 1 = למטה)

  return (
    <motion.div
      className="progress-bar"
      style={{
        scaleX: scrollYProgress,       // רוחב לפי התקדמות הגלילה
        transformOrigin: "left",
        position: "fixed",
        top: 0,
        left: 0,
        right: 0,
        height: 4,
        background: "#3B82F6",
        zIndex: 999
      }}
    />
  );
}
// progress bar שגדל ככל שגוללים למטה — בלי GSAP, בלי ScrollTrigger

MotionValue — הסוד מאחורי הביצועים: scrollYProgress הוא MotionValue — ערך מיוחד של Motion שמתעדכן בלי לגרום ל-React re-render. כשגוללים, scrollYProgress משתנה מ-0 ל-1, וה-progress bar מתעדכן — אבל React לא מרנדר מחדש את הרכיב. זו הסיבה שאנימציות scroll ב-Motion חלקות — הן עוקפות את ה-React reconciliation.

useTransform — מיפוי ערכים

function ParallaxHero() {
  const { scrollYProgress } = useScroll();

  // מיפוי: כשגוללים 0-100%, ה-y נע 0 עד -150
  const y = useTransform(scrollYProgress, [0, 1], [0, -150]);

  // מיפוי: כשגוללים 0-50%, opacity יורד מ-1 ל-0
  const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);

  // מיפוי: כשגוללים 0-30%, ה-scale יורד מ-1 ל-0.8
  const scale = useTransform(scrollYProgress, [0, 0.3], [1, 0.8]);

  return (
    <motion.div
      className="hero"
      style={{
        y,                              // motion MotionValues כ-style
        opacity,
        scale,
        height: "100vh",
        display: "flex",
        alignItems: "center",
        justifyContent: "center"
      }}
    >
      <h1>כותרת Hero עם Parallax</h1>
    </motion.div>
  );
}
// כשגוללים: ה-hero זז למעלה, מתפוגג, ומתכווץ — parallax מלא!

useScroll עם target — scroll של אלמנט ספציפי

import { useRef } from "react";
import { motion, useScroll, useTransform } from "motion/react";

function SectionReveal() {
  const sectionRef = useRef(null);

  // עוקב אחרי ה-section, לא אחרי כל הדף
  const { scrollYProgress } = useScroll({
    target: sectionRef,                 // עוקב אחרי אלמנט ספציפי
    offset: ["start end", "end start"]  // start=כניסה, end=יציאה
  });

  // opacity: 0 → 1 כשנכנס, 1 → 0 כשיוצא
  const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]);

  // y: 50 → 0 כשנכנס, 0 → -50 כשיוצא
  const y = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [50, 0, 0, -50]);

  return (
    <motion.section
      ref={sectionRef}
      style={{ opacity, y }}
    >
      <h2>Section שנכנס ויוצא בצורה חלקה</h2>
      <p>תוכן ה-section...</p>
    </motion.section>
  );
}

// offset מסביר "מתי מתחיל" ו"מתי מסתיים":
// ["start end", "end start"] = מתחיל כשתחילת האלמנט מגיע לתחתית ה-viewport
//                               מסתיים כשסוף האלמנט עובר את ראש ה-viewport
// זה מקביל ל-ScrollTrigger עם start: "top bottom" ו-end: "bottom top"

שרשרת useTransform — אפקטים מורכבים

function ComplexParallax() {
  const ref = useRef(null);
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"]
  });

  // שכבות parallax — כל שכבה זזה במהירות שונה
  const bgY = useTransform(scrollYProgress, [0, 1], ["-10%", "10%"]);
  const contentY = useTransform(scrollYProgress, [0, 1], ["0%", "0%"]);
  const floatY = useTransform(scrollYProgress, [0, 1], ["20%", "-20%"]);

  // סיבוב שמגיב ל-scroll
  const rotate = useTransform(scrollYProgress, [0, 1], [0, 360]);

  // צבע רקע שמשתנה עם הגלילה
  const backgroundColor = useTransform(
    scrollYProgress,
    [0, 0.5, 1],
    ["#1a1a2e", "#16213e", "#0f3460"]
  );

  return (
    <motion.section ref={ref} style={{ backgroundColor, position: "relative" }}>
      <motion.div className="bg-layer" style={{ y: bgY }} />
      <motion.div className="content-layer" style={{ y: contentY }}>
        <h2>תוכן</h2>
      </motion.div>
      <motion.div className="float-layer" style={{ y: floatY, rotate }} />
    </motion.section>
  );
}
7 דקות עשו עכשיו: progress bar + parallax hero
  1. העתיקו את ScrollProgress מלמעלה — הוסיפו progress bar fixed בראש הדף
  2. צרו דף ארוך (כמה sections עם תוכן) — וגללו. ה-progress bar צריך לגדול
  3. הוסיפו ParallaxHero מלמעלה כ-section הראשון — הוא צריך להתפוגג ולזוז למעלה כשגוללים
  4. שנו את ה-offset של useTransform ל-[0, 0.3] במקום [0, 0.5] ל-opacity — האפקט צריך להיות מהיר יותר
  5. הוסיפו useTransform נוסף: rotate שנע מ-0 ל-10 — כדי שה-hero מסתובב קצת בזמן הפוגה
useScroll ב-SSR (Next.js) — שימו לב

useScroll משתמש ב-window.scrollY שלא קיים בצד שרת (SSR). ב-Next.js, עטפו את הרכיב ב-"use client" (כבר ברור ב-Next.js 13+). אם אתם רואים "window is not defined" — זו הסיבה. פתרון: הוסיפו "use client" בראש הקובץ, או עטפו ב-dynamic import עם ssr: false. AI כמו Bolt ו-v0 לפעמים שוכחים את זה — בדקו.

מתחיל בינוני 10 דקות השוואה

6.9 GSAP vs Motion — טבלת החלטה

אחרי שלמדתם גם GSAP (פרקים 4-5) וגם Motion (הפרק הזה), השאלה הגדולה: מתי מה? התשובה הקצרה: Motion ל-React UI, GSAP לכל השאר. אבל בואו נפרט:

טבלת החלטה: GSAP מול Motion
קריטריוןGSAPMotion
Frameworkכל דבר — vanilla JS, React, Vue, Svelte, WordPress, static HTMLReact (מלא), Vue (בטא), vanilla (חלקי)
גישהImperative — gsap.to(), gsap.timeline()Declarative — props על JSX
Exit animationsניהול ידני של DOM lifecycleAnimatePresence — שורה אחת
Layout animationsFLIP plugin (ידני)layout prop / layoutId — אוטומטי
Scroll animationsScrollTrigger — עוצמתי מאוד (pin, snap, batch, horizontal)useScroll + useTransform — בסיסי אבל חלק
Spring physicsאין מובנה (אפשר CustomEase)ברירת מחדל — stiffness, damping, mass
Timelinesgsap.timeline() — עוצמתי, labels, position parameterאין timeline מובנה. Variants עם orchestration כתחליף
SVG animationsמצוין — MorphSVG, DrawSVG, MotionPathבסיסי — pathLength, motion.path
Text animationsSplitText — חלוקה לתווים/מילים/שורותאין מובנה — צריך ספריית צד שלישי
ביצועיםמהיר מאוד — מותאם ל-60fpsטוב — MotionValues עוקפים re-renders
Bundle size~25KB min (core) + ~10KB (ScrollTrigger)~18KB min (tree-shakeable)
AI code generationAI יודע GSAP טוב — הרבה דוגמאות באינטרנטAI מצוין עם Motion — קוד declarative קל לייצר
עקומת למידהבינונית — צריך ללמוד API גדולקלה-בינונית — אם מכירים React, Motion טבעי
רישיוןחינם לרוב. פלאגינים מתקדמים (MorphSVG, DrawSVG) = Clubחינם לגמרי, MIT license
עץ החלטה: GSAP או Motion?
השאלהאם כןאם לא
האם הפרויקט ב-React/Next.js?↓ המשך (Motion אפשרי)→ GSAP (Motion לא רלוונטי)
צריך exit animations (modals, pages, lists)?→ Motion (AnimatePresence)↓ המשך
צריך layout animations (magic move, reorder)?→ Motion (layout/layoutId)↓ המשך
צריך scroll מתקדם (pin, snap, horizontal)?→ GSAP ScrollTrigger↓ המשך
צריך timelines מורכבים (sequence, labels)?→ GSAP↓ המשך
צריך SVG מתקדם (morph, draw, path)?→ GSAP↓ המשך
צריך text splitting (SplitText)?→ GSAP↓ המשך
אנימציות UI רגילות (hover, tap, entrance)?→ Motion (פשוט יותר ב-React)→ GSAP

התשובה הפרקטית ל-Vibe Coders: בפרויקט React/Next.js, השתמשו ב-Motion כברירת מחדל לכל אנימציות UI — hover effects, entrance animations, exit animations, layout changes, gestures. כשצריך משהו ש-Motion לא יכול — scroll מתקדם (pin, snap), timelines מורכבים, SVG morph, text splitting — הוסיפו GSAP בנוסף. הם לא מתנגשים. אפשר להשתמש בשניהם באותו פרויקט. הכלל: Motion ל-UI layer, GSAP ל-wow layer.

3 דקות עשו עכשיו: בחרו כלי לפרויקט שלכם
  1. חשבו על הפרויקט האחרון שבניתם (או שאתם מתכננים)
  2. עברו על עץ ההחלטה למעלה — מה הייתם בוחרים?
  3. אם הפרויקט ב-React — סביר שתצטרכו Motion + GSAP ביחד. Motion לכפתורים, modals, page transitions. GSAP ל-hero animation, scroll storytelling, text effects
  4. אם הפרויקט לא ב-React — GSAP הוא הבחירה. Motion לא רלוונטי (חוץ מ-vanilla, שעדיין מוגבל)
מתחיל בינוני 10 דקות פרומפטים

6.10 פרומפטים AI ל-Motion — 5 פרומפטים React-specific

Motion ו-AI הם שילוב מושלם. הקוד של Motion הוא declarative — קל לתאר בפרומפט ו-AI מייצר קוד נקי. "add a fade-in animation" עם Motion זה שורה אחת. אבל פרומפט מדויק יותר ייתן תוצאה מקצועית יותר. הנה 5 פרומפטים שעובדים:

פרומפט 1: Component entrance animations (מתחיל)

Add entrance animations to this React page using the motion package
(import from "motion/react", NOT "framer-motion").

For each section heading: motion.h2 with initial={{ opacity: 0, y: 20 }}
and animate={{ opacity: 1, y: 0 }}.

For card grids: wrap the container in motion.div with variants that include
staggerChildren: 0.1. Each card should be motion.div with variants for
hidden (opacity: 0, y: 30) and visible (opacity: 1, y: 0).

Use whileInView instead of animate for below-the-fold content:
whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, amount: 0.3 }}

Transition: type "spring" with stiffness 100 and damping 15.
Do NOT use useEffect for any animations — everything should be declarative props.

פרומפט 2: Modal עם AnimatePresence (בינוני)

Create a Modal component in React using motion from "motion/react".

The modal should have:
1. An overlay (motion.div) that fades in/out: initial/exit opacity 0,
   animate opacity 1. Background: rgba(0,0,0,0.5). onClick closes modal.
2. A modal panel (motion.div) that scales and fades:
   initial={{ opacity: 0, scale: 0.9, y: 20 }}
   animate={{ opacity: 1, scale: 1, y: 0 }}
   exit={{ opacity: 0, scale: 0.9, y: 20 }}
3. Wrap both in AnimatePresence with mode="wait"
4. Transition: type "spring", stiffness 300, damping 25

The parent component should render:
<AnimatePresence>
  {isOpen && <Modal onClose={() => setIsOpen(false)} />}
</AnimatePresence>

Important: the modal must have a unique key prop.
Stop body scroll when modal is open (overflow: hidden on body).
Add onKeyDown handler for Escape key to close.

פרומפט 3: Page transitions ב-Next.js (בינוני)

Add page transition animations to this Next.js App Router project
using motion from "motion/react".

Create a PageTransition wrapper component:
- Wraps {children} in AnimatePresence mode="wait"
- Each page content wrapped in motion.div with:
  initial={{ opacity: 0, y: 10 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: -10 }}
  transition={{ duration: 0.3, ease: "easeInOut" }}
- Key should be based on pathname (use usePathname from next/navigation)

Place the PageTransition in the root layout.tsx, wrapping {children}.
Mark it as "use client" since it uses hooks.

Test: navigating between pages should show a smooth crossfade.
Do NOT use framer-motion package — use motion package with "motion/react" import.

פרומפט 4: Drag-to-reorder list (מתקדם)

Build a drag-to-reorder list component in React using motion
from "motion/react" with the Reorder module.

Import { Reorder } from "motion/react".
Use Reorder.Group and Reorder.Item components.

The list should:
1. Display 5 items with text and a drag handle icon
2. Each Reorder.Item has layout prop for smooth reordering animation
3. whileDrag: scale 1.02, boxShadow "0 5px 15px rgba(0,0,0,0.15)"
4. transition: type "spring", stiffness 300, damping 25
5. State managed with useState array, updated via onReorder callback

Add a subtle spring animation when items settle into new positions.
The drag handle should use cursor: grab (cursor: grabbing while dragging).

Style each item as a card with padding, border-radius, and subtle border.

פרומפט 5: Scroll-driven landing page (מתקדם)

Create a scroll-driven landing page in React using motion
from "motion/react". Use useScroll and useTransform hooks.

Sections:
1. Hero: Fixed progress bar at top using scrollYProgress → scaleX.
   Hero text fades out and moves up as user scrolls (useTransform).
2. Features: 3 cards with whileInView entrance animation.
   Use variants with staggerChildren: 0.15 on the container.
3. Showcase: Full-width image with parallax using useScroll with
   target ref and useTransform for y movement.
4. CTA: Scale up animation using scroll progress of that section.

Each section should use its own useScroll with target ref and
offset: ["start end", "end start"].

Do NOT use GSAP or ScrollTrigger — pure Motion hooks only.
Import everything from "motion/react".
Add "use client" at the top for Next.js compatibility.
15 דקות תרגיל: השתמשו ב-AI ליצירת Motion component
  1. בחרו כלי AI — Bolt, Lovable, v0, Cursor, או Claude
  2. השתמשו בפרומפט 2 (Modal עם AnimatePresence) כנקודת התחלה
  3. הריצו את הפרומפט וצפו בתוצאה
  4. בדקו: האם ה-AI ייבא מ-"motion/react" (ולא "framer-motion")? האם יש key על ה-modal? האם ה-exit animation עובד?
  5. שפרו: הוסיפו לפרומפט "Add a staggered entrance for modal content — title first, then body, then buttons with 0.1s stagger"
  6. בונוס: נסו פרומפט 4 (drag-to-reorder) — בדקו שהגרירה עובדת חלק ושהאיטמים מתאנמים למיקום החדש

קריטריון הצלחה: ה-modal נפתח עם animation, נסגר עם exit animation, ויש overlay שמתפוגג. אם ה-AI הוציא "framer-motion" — שנו ידנית ל-"motion/react".

מתקדם 15 דקות production

6.11 Professional Patterns — Modal, Page Transition, List Reorder

בסעיפים הקודמים למדתם את כל הכלים. עכשיו נרכיב אותם ל-3 patterns מקצועיים שתשתמשו בהם שוב ושוב בפרויקטים אמיתיים. כל pattern הוא production-ready — אפשר להעתיק ולהתאים.

Pattern 1: Modal מקצועי עם overlay, stagger, ו-escape key

import { motion, AnimatePresence } from "motion/react";
import { useEffect } from "react";

// Overlay variants
const overlayVariants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1 }
};

// Modal variants עם stagger לילדים
const modalVariants = {
  hidden: {
    opacity: 0,
    scale: 0.95,
    y: 20
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: {
      type: "spring",
      stiffness: 300,
      damping: 25,
      staggerChildren: 0.1,
      delayChildren: 0.1
    }
  },
  exit: {
    opacity: 0,
    scale: 0.95,
    y: 20,
    transition: { duration: 0.2 }
  }
};

// Content item variants (title, body, buttons)
const itemVariants = {
  hidden: { opacity: 0, y: 10 },
  visible: { opacity: 1, y: 0 }
};

function Modal({ isOpen, onClose, title, children }) {
  // Escape key
  useEffect(() => {
    const handleEsc = (e) => e.key === "Escape" && onClose();
    window.addEventListener("keydown", handleEsc);
    return () => window.removeEventListener("keydown", handleEsc);
  }, [onClose]);

  // Lock body scroll
  useEffect(() => {
    if (isOpen) document.body.style.overflow = "hidden";
    return () => { document.body.style.overflow = ""; };
  }, [isOpen]);

  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          key="modal-overlay"
          variants={overlayVariants}
          initial="hidden"
          animate="visible"
          exit="hidden"
          onClick={onClose}
          style={{
            position: "fixed", inset: 0, zIndex: 1000,
            background: "rgba(0,0,0,0.5)",
            display: "flex", alignItems: "center", justifyContent: "center"
          }}
        >
          <motion.div
            key="modal-panel"
            variants={modalVariants}
            initial="hidden"
            animate="visible"
            exit="exit"
            onClick={(e) => e.stopPropagation()}
            style={{
              background: "white", borderRadius: 16, padding: 32,
              maxWidth: 500, width: "90%", maxHeight: "80vh", overflowY: "auto"
            }}
          >
            <motion.h2 variants={itemVariants}>{title}</motion.h2>
            <motion.div variants={itemVariants}>{children}</motion.div>
            <motion.button
              variants={itemVariants}
              whileHover={{ scale: 1.02 }}
              whileTap={{ scale: 0.98 }}
              onClick={onClose}
              style={{ marginTop: 20, padding: "10px 24px", borderRadius: 8 }}
            >
              סגור
            </motion.button>
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Pattern 2: Page Transitions ב-Next.js App Router

// app/components/PageTransition.jsx
"use client";
import { motion, AnimatePresence } from "motion/react";
import { usePathname } from "next/navigation";

const pageVariants = {
  initial: { opacity: 0, y: 8 },
  enter: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.3, ease: "easeOut" }
  },
  exit: {
    opacity: 0,
    y: -8,
    transition: { duration: 0.2, ease: "easeIn" }
  }
};

export default function PageTransition({ children }) {
  const pathname = usePathname();

  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        variants={pageVariants}
        initial="initial"
        animate="enter"
        exit="exit"
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}

// app/layout.tsx — שימוש
// import PageTransition from "./components/PageTransition";
//
// export default function RootLayout({ children }) {
//   return (
//     <html>
//       <body>
//         <Navbar />
//         <PageTransition>{children}</PageTransition>
//       </body>
//     </html>
//   );
// }

Pattern 3: List Reorder עם Drag

import { Reorder } from "motion/react";
import { useState } from "react";

const initialItems = [
  { id: "1", title: "למד Motion", done: false },
  { id: "2", title: "בנה modal", done: true },
  { id: "3", title: "הוסף page transitions", done: false },
  { id: "4", title: "תרגל layout animations", done: false },
  { id: "5", title: "deploy!", done: false }
];

function ReorderableList() {
  const [items, setItems] = useState(initialItems);

  return (
    <Reorder.Group
      axis="y"
      values={items}
      onReorder={setItems}
      style={{ listStyle: "none", padding: 0 }}
    >
      {items.map(item => (
        <Reorder.Item
          key={item.id}
          value={item}
          whileDrag={{
            scale: 1.03,
            boxShadow: "0 8px 25px rgba(0,0,0,0.15)",
            cursor: "grabbing"
          }}
          transition={{ type: "spring", stiffness: 300, damping: 25 }}
          style={{
            padding: "16px 20px",
            marginBottom: 8,
            background: "white",
            borderRadius: 12,
            border: "1px solid #e2e8f0",
            cursor: "grab",
            display: "flex",
            alignItems: "center",
            gap: 12
          }}
        >
          <span style={{ fontSize: 20 }}>☰</span>
          <span style={{
            textDecoration: item.done ? "line-through" : "none",
            opacity: item.done ? 0.5 : 1
          }}>
            {item.title}
          </span>
        </Reorder.Item>
      ))}
    </Reorder.Group>
  );
}
// גוררים item — הוא "צף" עם shadow. משחררים — הרשימה מתאנמת
// למיקום החדש עם spring. הכל בשורות בודדות של קוד.
Motion ב-production — 3 טעויות נפוצות

(1) initial={false} שנשכח: כשהדף נטען, כל motion component מריץ את animate מ-initial. אם יש 50 כרטיסים עם initial={{ opacity: 0 }}, הם כולם יעשו fade-in יחד — שנראה מוזר. פתרון: השתמשו ב-whileInView לרכיבים below-the-fold, או initial={false} לרכיבים שצריכים להופיע מיד. (2) layoutId כפול: אם שני רכיבים עם אותו layoutId נמצאים ב-DOM בו-זמנית — Motion מתבלבל ואנימציה מוזרה. כל layoutId חייב להיות unique בכל רגע. (3) Bundle size: Motion היא tree-shakeable — אם מייבאים רק { motion } בלי AnimatePresence, useScroll, Reorder — ה-bundle קטן יותר. אל תייבאו * כי זה מנפח.

5 דקות עשו עכשיו: הוסיפו whileInView לכרטיסים
  1. אם יש לכם רכיב עם כרטיסים/רשימה ב-React — הוסיפו whileInView:
  2. whileInView={{ opacity: 1, y: 0 }}
  3. initial={{ opacity: 0, y: 30 }}
  4. viewport={{ once: true, amount: 0.3 }}
  5. once: true = האנימציה רצה רק פעם אחת. amount: 0.3 = מתחיל כש-30% מהאלמנט נראה
  6. זו האלטרנטיבה של Motion ל-ScrollTrigger.batch() — הרבה יותר פשוטה ב-React
צ'קליסט — מה לבדוק לפני שמפרסמים Motion animations
// prefers-reduced-motion ב-Motion
import { useReducedMotion } from "motion/react";

function MyComponent() {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      animate={{ x: 100 }}
      transition={shouldReduceMotion
        ? { duration: 0 }                // בלי אנימציה
        : { type: "spring", stiffness: 200 }  // אנימציה רגילה
      }
    />
  );
}

// או גלובלי — ב-CSS:
// @media (prefers-reduced-motion: reduce) {
//   *, *::before, *::after {
//     animation-duration: 0.01ms !important;
//     transition-duration: 0.01ms !important;
//   }
// }
שגרת עבודה: Motion animations ב-React
שלבפעולהזמן
1. Setupnpm install motion. ייבוא מ-"motion/react". בדקו שמייבאים את ה-package הנכון2 דקות
2. Entranceהוסיפו initial + animate (או whileInView) לרכיבים מרכזיים. התחילו מ-hero ו-headings15 דקות
3. Interactionsהוסיפו whileHover + whileTap לכפתורים ולכרטיסים. בחרו spring או ease לפי הסגנון10 דקות
4. Variantsארגנו אנימציות חוזרות ל-variants. הוסיפו stagger ל-grids ורשימות15 דקות
5. Exit/Layoutהוסיפו AnimatePresence ל-modals, notifications, page transitions. layout ל-lists ו-grids דינמיים15 דקות
6. Scrollאם צריך scroll animations — הוסיפו useScroll + useTransform. Progress bar, parallax, fade-in10 דקות
7. Testבדקו: exit animations עובדים? layout smooth? mobile touch? prefers-reduced-motion?10 דקות
בדקו את עצמכם — מה למדתם בפרק
סיכום הפרק
מה בפרק הבא

עכשיו אתם יודעים שתי ספריות אנימציה: GSAP (imperative, עוצמתי, universal) ו-Motion (declarative, React-native, elegant). בפרק 7 תלמדו Lenis — ספריית smooth scroll שמשנה את כל חוויית הגלילה. Lenis לא מחליף את ScrollTrigger או useScroll — הוא פועל מתחת להם, ומשפר את הגלילה עצמה. תלמדו איך Lenis עובד, איך לשלב אותו עם GSAP וגם עם Motion, ולמה כל אתר premium משתמש ב-smooth scroll. אחרי Lenis, תהיה לכם שליטה מלאה על כל שכבת האנימציה — מ-CSS ועד scroll experience שלם.

→ פרק קודם: ScrollTrigger — קסם שמונע מגלילה | פרק הבא: Lenis — גלילה חלקה ←