+ {/* Secondary sign-up options continue the same cascade just after the
+ fields (delay continues from the form group's three children above:
+ 0.04 + 3 × 0.08). */}
+
+
+
+
+
+
+ or
+
+
-
+
+
+
+
);
};
diff --git a/frontend/apps/artcraft-webapp/src/components/motion/reveal.tsx b/frontend/apps/artcraft-webapp/src/components/motion/reveal.tsx
index c208ee26d3..e7760a7151 100644
--- a/frontend/apps/artcraft-webapp/src/components/motion/reveal.tsx
+++ b/frontend/apps/artcraft-webapp/src/components/motion/reveal.tsx
@@ -16,6 +16,23 @@ import type { ElementType, ReactNode } from "react";
import { motion, useReducedMotion, type Variants } from "framer-motion";
import { DUR_SLOW, EASE_OUT } from "../../lib/motion";
+// `motion(as)` returns a BRAND-NEW component on every call. Calling it during
+// render would hand each render a different component type, so React would
+// unmount and remount the whole subtree on every re-render — stealing focus
+// from any inside (this is what broke the login form: typing a
+// character re-rendered the page and blurred the field). Cache one motion
+// component per element type so the reference stays stable across renders.
+const motionTagCache = new Map();
+
+function motionTag(as: ElementType): ElementType {
+ let cached = motionTagCache.get(as);
+ if (!cached) {
+ cached = motion(as) as ElementType;
+ motionTagCache.set(as, cached);
+ }
+ return cached;
+}
+
interface RevealProps {
children: ReactNode;
/** Render as a different element/component (e.g. "section", "li", Link). */
@@ -49,7 +66,7 @@ export function RevealGroup({
className?: string;
}) {
const reduceMotion = useReducedMotion();
- const MotionTag = motion(as);
+ const MotionTag = motionTag(as);
const variants: Variants = {
initial: {},
@@ -83,22 +100,30 @@ export function Reveal({
className,
}: RevealProps) {
const reduceMotion = useReducedMotion();
- const MotionTag = motion(as);
-
- const variants: Variants = {
- initial: reduceMotion ? { opacity: 0 } : { opacity: 0, y },
- animate: {
- opacity: 1,
- y: 0,
- transition: { duration: DUR_SLOW, ease: EASE_OUT, delay },
- },
- };
+ const MotionTag = motionTag(as);
// When `inView` is left undefined the element behaves as a stagger child:
// it inherits the parent RevealGroup's orchestration and must NOT drive its
// own initial/animate props (that would override the cascade).
const standalone = inView !== undefined;
+ // Only a standalone reveal sets its own `delay`. For a stagger child the
+ // delay MUST be omitted: framer-motion lets a child's explicit transition
+ // `delay` override the parent's `staggerChildren`/`delayChildren`, so leaving
+ // it in (even as 0) collapses the cascade and every child animates at once.
+ const variants: Variants = {
+ initial: reduceMotion ? { opacity: 0 } : { opacity: 0, y },
+ animate: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: DUR_SLOW,
+ ease: EASE_OUT,
+ ...(standalone ? { delay } : {}),
+ },
+ },
+ };
+
return (
{
/>
-
-
+
-
-
-
-
-
-
- or
-
-
+ {/* Secondary sign-in options pick up the same cascade just after the
+ fields (delay continues from the form group's three children above:
+ 0.04 + 3 × 0.08). */}
+
+
+
+
+
+
+ or
+
+
-
-
-
+
+
+
-
-
- Don't have an account?{" "}
-
- Sign up
-
-
-
+
+
+ Don't have an account?{" "}
+
+ Sign up
+
+
+
>
);
diff --git a/frontend/apps/artcraft-webapp/src/pages/signup/signup.tsx b/frontend/apps/artcraft-webapp/src/pages/signup/signup.tsx
index 93e6ef5d07..1495f3236c 100644
--- a/frontend/apps/artcraft-webapp/src/pages/signup/signup.tsx
+++ b/frontend/apps/artcraft-webapp/src/pages/signup/signup.tsx
@@ -1,7 +1,7 @@
import { Link, useNavigate } from "react-router-dom";
import { AuthHeader, AuthFooter, SignupForm } from "../../components/auth";
import Seo from "../../components/seo";
-import { Reveal, RevealGroup } from "../../components/motion/reveal";
+import { Reveal } from "../../components/motion/reveal";
const Signup = () => {
const navigate = useNavigate();
@@ -14,26 +14,25 @@ const Signup = () => {
/>
-
-
- navigate("/welcome")}
- signupSource="artcraft"
- />
-
+ {/* SignupForm runs its own field cascade internally (see signup-form.tsx).
+ The footer picks up the tail of that cascade — the form's six staggered
+ steps land by ~0.36s, so the footer follows at ~0.44s. */}
+ navigate("/welcome")}
+ signupSource="artcraft"
+ />
-
-
- Already have an account?{" "}
-
- Log in
-
-
-
-
+
+
+ Already have an account?{" "}
+
+ Log in
+
+
+
>
);
};