Workspace / component / animations
Antigravity
paxfx add blocks/component-animations-antigravitylocal onlyTier: high. Reduced motion: Children hold their layout positions.
"use client";
import { Children, type CSSProperties, type ReactNode } from "react";
import { cn, useInView, useReducedMotion } from "@pax-sh/blocks";
export interface AntigravityProps {
/** Multiplier on drift distance; 1 is the tuned default. */
amplitude?: number;
/** Base seconds for one orbit; each child gets a deterministic offset. */
period?: number;
className?: string;
children?: ReactNode;
}
const DEFAULT_ITEMS: ReactNode[] = [
<span
key="chip-motion"
className="rounded-full border border-[var(--pax-border,rgba(18,17,15,0.12))] bg-[var(--pax-paper,#f6f2ea)] px-4 py-2 text-sm font-semibold text-[var(--pax-ink,#12110f)]"
>
Motion
</span>,
<span
key="dot-acid"
aria-hidden="true"
className="block h-10 w-10 rounded-full bg-[var(--pax-acid,#c8ff00)]"
/>,
<span
key="chip-canvas"
className="rounded-full bg-[var(--pax-acid,#c8ff00)] px-4 py-2 text-sm font-semibold text-[var(--pax-ink,#12110f)]"
>
Canvas
</span>,
<span
key="square-paper"
aria-hidden="true"
className="block h-8 w-8 rotate-12 rounded-lg border border-[var(--pax-paper,#f6f2ea)] opacity-60"
/>,
<span
key="chip-layout"
className="rounded-full border border-[var(--pax-paper,#f6f2ea)] px-4 py-2 text-sm font-semibold text-[var(--pax-paper,#f6f2ea)]"
>
Layout
</span>,
];
/**
* Children float free of the layout on independent slow orbits. Each child
* gets a deterministic duration, phase, and drift path from its index, so
* the field never syncs up and captures are reproducible. Motion is CSS
* keyframes on transform only; reduced motion holds every child in place.
*/
export function Antigravity({ amplitude = 1, period = 9, className, children }: AntigravityProps) {
const reduced = useReducedMotion();
const [ref, inView] = useInView<HTMLDivElement>({ threshold: 0.2 });
const animate = inView && !reduced;
const items =
children !== undefined && children !== null ? Children.toArray(children) : DEFAULT_ITEMS;
const amp = Math.max(0, amplitude);
const orbitStyle = (i: number): CSSProperties | undefined => {
if (!animate) return undefined;
const duration = period * (1 + ((i * 37) % 40) / 100);
const delay = -duration * (((i * 61) % 100) / 100);
return {
animation: `pax-antigravity-orbit-${(i % 3) + 1} ${duration.toFixed(2)}s ease-in-out ${delay.toFixed(2)}s infinite`,
};
};
return (
<div
ref={ref}
className={cn(
"relative isolate min-h-[240px] overflow-hidden rounded-2xl border border-[var(--pax-border,rgba(18,17,15,0.12))]",
"bg-[var(--pax-night,#0b0d10)] text-[var(--pax-paper,#f6f2ea)]",
className,
)}
data-pax-asset="component-animations-antigravity"
style={{ "--pax-anti-amp": amp } as CSSProperties}
>
<style>{`
@keyframes pax-antigravity-orbit-1 {
0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
25% { transform: translate3d(calc(var(--pax-anti-amp, 1) * 10px), calc(var(--pax-anti-amp, 1) * -14px), 0) rotate(2deg); }
50% { transform: translate3d(calc(var(--pax-anti-amp, 1) * -6px), calc(var(--pax-anti-amp, 1) * -22px), 0) rotate(-1.5deg); }
75% { transform: translate3d(calc(var(--pax-anti-amp, 1) * -12px), calc(var(--pax-anti-amp, 1) * -8px), 0) rotate(1deg); }
}
@keyframes pax-antigravity-orbit-2 {
0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
25% { transform: translate3d(calc(var(--pax-anti-amp, 1) * -14px), calc(var(--pax-anti-amp, 1) * 8px), 0) rotate(-2deg); }
50% { transform: translate3d(calc(var(--pax-anti-amp, 1) * -4px), calc(var(--pax-anti-amp, 1) * 18px), 0) rotate(2.5deg); }
75% { transform: translate3d(calc(var(--pax-anti-amp, 1) * 10px), calc(var(--pax-anti-amp, 1) * 6px), 0) rotate(-1deg); }
}
@keyframes pax-antigravity-orbit-3 {
0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
25% { transform: translate3d(calc(var(--pax-anti-amp, 1) * 8px), calc(var(--pax-anti-amp, 1) * 12px), 0) rotate(1.5deg); }
50% { transform: translate3d(calc(var(--pax-anti-amp, 1) * 18px), calc(var(--pax-anti-amp, 1) * -4px), 0) rotate(-2deg); }
75% { transform: translate3d(calc(var(--pax-anti-amp, 1) * 4px), calc(var(--pax-anti-amp, 1) * -12px), 0) rotate(1deg); }
}
`}</style>
<div className="flex min-h-[240px] flex-wrap items-center justify-center gap-8 p-10">
{items.map((item, i) => (
<div key={i} className="will-change-transform" style={orbitStyle(i)}>
{item}
</div>
))}
</div>
</div>
);
}