Hooks
FLIP Animation Hooks
Core hooks for building FLIP (First-Last-Invert-Play) animations with WAAPI.
FLIP Animation Hooks
Low-level hooks for building custom FLIP animations using the Web Animations API. These are the primitives that power the Reorder component.
Imports
import {
useElementRegistry,
usePositionCapture,
useFLIPAnimation,
useAnimationOrchestrator,
// Constants
TIMING,
TRANSFORMS,
EFFECTS,
EASINGS,
PRESETS,
getResponsiveDuration,
getResponsiveStagger,
} from '@mks2508/mks-ui/react';Architecture
The FLIP system is composed of four layers:
useAnimationOrchestrator ← High-level: 8-step FLIP sequence
│
├── useFLIPAnimation ← Execute WAAPI animations
│
├── usePositionCapture ← Calculate FLIP deltas
│
└── useElementRegistry ← Track DOM elementsuseElementRegistry
Track DOM elements by ID for FLIP calculations.
const registry = useElementRegistry({
onRegister: (id, element) => console.log(`Registered: ${id}`),
onUnregister: (id) => console.log(`Unregistered: ${id}`),
});
// Register element
registry.register('item-1', document.getElementById('item-1'));
// API
registry.register(id, element); // Register element
registry.unregister(id); // Remove element
registry.get(id); // Get element by ID
registry.getAll(); // Map<string, HTMLElement>
registry.has(id); // Check if registered
registry.clear(); // Remove all
registry.size; // CountReturn Type
interface IElementRegistryAPI {
register: (id: string, element: HTMLElement | null) => void;
unregister: (id: string) => void;
get: (id: string) => HTMLElement | undefined;
getAll: () => Map<string, HTMLElement>;
has: (id: string) => boolean;
clear: () => void;
size: number;
}usePositionCapture
Capture element positions and calculate FLIP deltas.
const capture = usePositionCapture({
threshold: TIMING.MOVEMENT_THRESHOLD, // Ignore movements < 2px
});
// Capture current positions of all registered elements
capture.capture(registry);
// Get individual position
const rect = capture.getPosition('item-1');
// Calculate delta between before/after
const delta = capture.calculateDelta('item-1', afterRect);
// { deltaX: 100, deltaY: 0, scaleX: 1, scaleY: 1 }Return Type
interface IPositionCaptureAPI {
capture: (registry: Map<string, HTMLElement>) => void;
getPosition: (id: string) => IPositionRect | undefined;
calculateDelta: (id: string, after: IPositionRect) => IFLIPDelta | null;
getLastPositions: () => Map<string, IPositionRect>;
}
interface IFLIPDelta {
deltaX: number;
deltaY: number;
scaleX: number;
scaleY: number;
}useFLIPAnimation
Execute FLIP animations using WAAPI with spring easing.
const flip = useFLIPAnimation();
// Animate single element
flip.animate(element, delta, {
duration: 300,
easing: EASINGS.MATERIAL.STANDARD,
});
// Animate multiple in parallel
flip.animateAll(
new Map([['item-1', delta1], ['item-2', delta2]]),
{ duration: 300 }
);
// Cancel
flip.cancel('item-1');
flip.cancelAll();
// Check state
flip.isAnimating('item-1');Return Type
interface IFLIPAnimationAPI {
animate: (element: HTMLElement, delta: IFLIPDelta, options?: IAnimationTiming) => Animation;
animateAll: (deltas: Map<string, IFLIPDelta>, options?: IAnimationTiming) => Animation[];
cancel: (id: string) => void;
cancelAll: () => void;
isAnimating: (id?: string) => boolean;
}useAnimationOrchestrator
High-level orchestrator composing all primitives into an 8-step FLIP sequence.
const orchestrator = useAnimationOrchestrator({
enterDuration: 300,
exitDuration: 200,
flipDuration: 300,
flipBehavior: 'siblings-after',
exitPositionStrategy: 'absolute-fixed',
onExitComplete: (id) => removeItem(id),
});8-Step Exit Sequence
- Capture BEFORE positions of all elements
- Position:absolute on exiting element with parent compensation
- Synchronous reflow via
getBoundingClientRect()(prevents flicker) - Capture AFTER positions
- Calculate INVERT deltas with
flipBehaviorfiltering - Per-item stagger exit animation (opacity/scale/blur)
- Promise.all wait for all animations
- Cleanup remove positioning, call
onExitComplete
Return Type
interface IAnimationOrchestratorAPI {
registerElement: (id: string, element: HTMLElement | null) => void;
unregisterElement: (id: string) => void;
startExit: (id: string) => Promise<void>;
startEnter: (id: string) => Promise<void>;
isAnimating: (id?: string) => boolean;
registry: Map<string, HTMLElement>;
}Animation Constants
TIMING
TIMING = {
ENTER_DURATION: 200, // ms
EXIT_DURATION: 180, // ms
FLIP_DURATION: 300, // ms
STAGGER_DELAY: 12, // ms
STAGGER_DELAY_MOBILE: 8, // ms
MOVEMENT_THRESHOLD: 2, // px - ignore smaller movements
}EASINGS
EASINGS = {
MATERIAL: {
STANDARD: 'cubic-bezier(0.4, 0, 0.2, 1)',
DECELERATE: 'cubic-bezier(0, 0, 0.2, 1)',
ACCELERATE: 'cubic-bezier(0.4, 0, 1, 1)',
SHARP: 'cubic-bezier(0.4, 0, 0.6, 1)',
},
PHYSICS: {
GENTLE: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
BOUNCE: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
ELASTIC: 'cubic-bezier(0.68, -0.6, 0.32, 1.6)',
},
SPRING: {
DEFAULT: 'linear(0, 0.006, 0.024 2.1%, ...)',
SNAPPY: 'linear(0, 0.003, 0.018 2.5%, ...)',
GENTLE: 'linear(0, 0.005, 0.025 2.8%, ...)',
},
}Responsive Helpers
// Reduce duration for reduced motion or mobile
getResponsiveDuration(300, { reducedMotion: true, isMobile: false });
// => 150 (50% for reduced motion)
getResponsiveDuration(300, { reducedMotion: false, isMobile: true });
// => 180 (60% for mobile)
getResponsiveStagger(20, true);
// => 10 (50% for mobile)Building Custom Animations
Example: Custom Staggered Exit
function useStaggeredExit(registry: Map<string, HTMLElement>) {
const capture = usePositionCapture();
const flip = useFLIPAnimation();
const exitAll = async (ids: string[], staggerMs = 20) => {
// 1. Capture positions
capture.capture(registry);
// 2. Execute staggered exits
const animations: Promise<void>[] = [];
ids.forEach((id, index) => {
const element = registry.get(id);
if (!element) return;
// Delay start based on index
setTimeout(() => {
const animation = element.animate(
{ opacity: [1, 0], transform: ['scale(1)', 'scale(0.85)'] },
{ duration: 200, easing: EASINGS.MATERIAL.DECELERATE }
);
animations.push(animation.finished);
}, index * staggerMs);
});
await Promise.all(animations);
};
return { exitAll };
}Related
- Reorder - Uses these hooks internally
- Morph - Shape morphing animations
- CSS State Machine - CSS data attributes for animation states