Skip to content

framer-motion-combat-juice

<!-- Loaded by combat-effects-upgrade when task involves card trajectories, hit-react animations, multi-hit stagger, or layout animations in combat components -->

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

<!-- Loaded by combat-effects-upgrade when task involves card trajectories, hit-react animations, multi-hit stagger, or layout animations in combat components -->

Agent definition

framer-motion-combat-juice.md

Framer Motion Combat Juice Reference

<!-- Loaded by combat-effects-upgrade when task involves card trajectories, hit-react animations, multi-hit stagger, or layout animations in combat components -->

Framer Motion renamed to **Motion** in 2025. Package: `framer-motion` → `motion`, imports: `framer-motion` → `motion/react`. API identical. Check package.json.

import { motion, AnimatePresence, useMotionValue, useSpring } from 'motion/react';
// Legacy: import from 'framer-motion' (same API)

Patterns below use `motion/react`. Replace with `framer-motion` if needed.

---

Current Animation Inventory and Upgrades

| # | Component | Current Pattern | Upgraded To | |---|-----------|----------------|-------------| | 1 | FramedCard — hover | scale(1.03) | scale(1.06) + 3D tilt toward cursor | | 2 | FramedCard — play exit | scale(1.3) + slide up | trajectory arc toward target slot | | 3 | PlayerCharacter — hit react | scale(0.85→1) | scale(0.85→1) + rotation wobble + spring overshoot | | 4 | CombatPopups — damage | float up, instant | cascading stagger 100ms per hit | | 5 | EnemyCharacter — idle | 4s breathing scale | add subtle sway (±2deg rotate) | | 6 | CombatArena — screen shake | CSS class toggle | add motion blur via filter | | 7 | CardHand — draw | fade in | slide from draw pile position via layoutId | | 8 | PlayerCharacter — status badge | spring scale pop | add jiggle on value change | | 9 | CardHand — reflow | spring layout | smooth reflow using layout prop |

---

Pattern 1: Card Play Trajectory

Card flies from hand to target slot. `layoutId` measures start/end positions automatically.

// src/components/FramedCard.tsx
import { motion, AnimatePresence } from 'motion/react';

interface FramedCardProps {
  card: Card;
  isPlaying: boolean;
  targetRef: React.RefObject<HTMLDivElement>;
}

export function FramedCard({ card, isPlaying, targetRef }: FramedCardProps) {
  return (
    <AnimatePresence mode="popLayout">
      {!isPlaying && (
        <motion.div
          key={card.id}
          layoutId={`card-${card.id}`}
          initial={{ opacity: 0, y: 40 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{
            // Arc trajectory: scale up briefly, fly toward target
            scale: [1, 1.15, 0.9],
            y: -120,
            opacity: 0,
            transition: {
              duration: 0.35,
              ease: [0.25, 0.46, 0.45, 0.94],
            },
          }}
          transition={{
            type: 'spring',
            stiffness: 400,
            damping: 28,
          }}
          className="framed-card"
        >
          <CardContent card={card} />
        </motion.div>
      )}
    </AnimatePresence>
  );
}

---

Pattern 2: Hand Reflow with Layout Animation

Remaining cards auto-animate into new positions. No manual position calculation.

// src/components/CardHand.tsx
import { motion, AnimatePresence } from 'motion/react';

interface CardHandProps {
  cards: Card[];
  onPlay: (card: Card) => void;
}

export function CardHand({ cards, onPlay }: CardHandProps) {
  return (
    // layout on the container propagates to children
    <motion.div layout className="card-hand">
      <AnimatePresence mode="popLayout">
        {cards.map((card, index) => (
          <motion.div
            key={card.id}
            layout  // automatically animates position changes
            initial={{ opacity: 0, scale: 0.8, y: 30 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.7, y: -20 }}
            transition={{
              type: 'spring',
              stiffness: 350,
              damping: 30,
              // Stagger cards as they enter
              delay: index * 0.05,
            }}
            onClick={() => onPlay(card)}
            style={{
              // Fan layout uses CSS custom property driven by index
              '--card-index': index,
              '--card-count': cards.length,
            } as React.CSSProperties}
          >
            <FramedCard card={card} />
          </motion.div>
        ))}
      </AnimatePresence>
    </motion.div>
  );
}

---

Pattern 3: Hit React with Spring Overshoot

Character squishes, overshoots past normal, settles. Spring physics handle interruption automatically.

// src/components/PlayerCharacter.tsx
import { motion, useAnimation } from 'motion/react';

export function PlayerCharacter({ isHit, hitType }: PlayerCharacterProps) {
  const controls = useAnimation();

  useEffect(() => {
    if (!isHit) return;

    // Fire-and-forget: squish + wobble sequence
    void controls.start({
      scale: [1, 0.82, 1.08, 0.96, 1],
      rotate: [0, hitType === 'left' ? -5 : 5, hitType === 'left' ? 3 : -3, 0],
      transition: {
        duration: 0.5,
        times: [0, 0.15, 0.35, 0.6, 1],
        ease: 'easeInOut',
      },
    });
  }, [isHit, hitType, controls]);

  return (
    <motion.div
      animate={controls}
      style={{ originX: 0.5, originY: 0.8 }} // pivot from feet
    >
      <CharacterSprite />
    </motion.div>
  );
}

---

Pattern 4: Multi-Hit Damage Stagger

Each damage number 100ms after previous, so players track individual values.

// src/components/CombatPopups.tsx
import { motion, AnimatePresence } from 'motion/react';

interface DamageEvent {
  id: string;
  value: number;
  x: number;
  y: number;
  timestamp: number;
}

interface CombatPopupsProps {
  damageEvents: DamageEvent[];
}

export function CombatPopups({ damageEvents }: CombatPopupsProps) {
  return (
    <AnimatePresence>
      {damageEvents.map((event, index) => (
        <motion.div
          key={event.id}
          className="damage-popup"
          initial={{ opacity: 0, scale: 0.6, y: 0 }}
          animate={{ opacity: 1, scale: 1.2, y: -20 }}
          exit={{ opacity: 0, scale: 0.8, y: -60 }}
          transition={{
            // Stagger each hit 100ms from the previous
            delay: index * 0.1,
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked