{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "arc-reveal-hero", "type": "registry:ui", "title": "Arc Reveal Hero", "description": "A multilingual greeting cycles on a calm white surface, then a curved black curtain rises from below to reveal the landing — the boundary stays a single smooth arc the whole way up, never a flat slide.", "dependencies": [ "motion" ], "files": [ { "path": "registry/ruixenui/arc-reveal-hero.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n animate,\n AnimatePresence,\n motion,\n useMotionValue,\n useReducedMotion,\n useTransform,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ── types ───────────────────────────────────────────────────── */\n\nexport type ArcRevealGreeting = {\n /** Greeting text in the target script */\n text: string;\n /** Optional `lang` attribute applied to the span (helps screen readers / font rendering) */\n lang?: string;\n};\n\nexport interface ArcRevealHeroProps {\n /** Greetings cycled before the arc reveal. */\n greetings?: ArcRevealGreeting[];\n /** How long each greeting is held on screen (ms). */\n greetingHold?: number;\n /** Duration of the curved curtain reveal (ms). */\n revealDuration?: number;\n /** Outer `
` class. Receives the *post-reveal* surface. */\n className?: string;\n /** Class for the intro (pre-reveal) overlay surface. */\n introClassName?: string;\n /** Class for the cycled greeting ``. */\n greetingClassName?: string;\n /** Class for the wrapper around `children` (the revealed content). */\n revealClassName?: string;\n /**\n * Optional `sessionStorage` key — when set, the intro plays only once per\n * session for the same key. Leave unset to replay on every mount.\n */\n storageKey?: string;\n /** Content shown after the curtain reveal (the \"landing\"). */\n children?: React.ReactNode;\n}\n\n/* ── defaults ────────────────────────────────────────────────── */\n\nconst DEFAULT_GREETINGS: ArcRevealGreeting[] = [\n { text: \"Quiet.\" },\n { text: \"Sharp.\" },\n { text: \"Calm.\" },\n { text: \"Crafted.\" },\n { text: \"Considered.\" },\n { text: \"Composed.\" },\n { text: \"Honest.\" },\n { text: \"Ready.\" },\n];\n\ntype Phase = \"intro\" | \"reveal\" | \"done\";\n\n/* ── component ───────────────────────────────────────────────── */\n\nexport function ArcRevealHero({\n greetings = DEFAULT_GREETINGS,\n greetingHold = 620,\n revealDuration = 1500,\n className,\n introClassName,\n greetingClassName,\n revealClassName,\n storageKey,\n children,\n}: ArcRevealHeroProps) {\n const prefersReducedMotion = useReducedMotion();\n\n const [phase, setPhase] = React.useState(\"intro\");\n const [index, setIndex] = React.useState(0);\n\n // Drive the arc shape from a single 0→1 progress.\n // The curve is a quadratic bezier with a fixed concavity (control point\n // sits 25 viewBox units below the chord), translated upward over time:\n // t=0 → chord at y=110 (off-screen below) → no curtain visible\n // t=1 → chord at y=-30 (off-screen above) → full-screen curtain\n const progress = useMotionValue(0);\n const arcPath = useTransform(progress, (p: number) => {\n const edge = 110 - p * 140;\n const control = edge + 25;\n return `M 0 ${edge} Q 50 ${control} 100 ${edge} L 100 110 L 0 110 Z`;\n });\n\n // Honor reduced-motion + replay-suppression on mount.\n React.useEffect(() => {\n if (prefersReducedMotion) {\n setPhase(\"done\");\n return;\n }\n if (storageKey && typeof window !== \"undefined\") {\n try {\n if (window.sessionStorage.getItem(storageKey) === \"done\") {\n setPhase(\"done\");\n }\n } catch {\n /* sessionStorage can throw in private mode — fall through */\n }\n }\n }, [prefersReducedMotion, storageKey]);\n\n // Greeting cycle.\n React.useEffect(() => {\n if (phase !== \"intro\") return;\n const isLast = index >= greetings.length - 1;\n if (isLast) {\n const t = window.setTimeout(() => setPhase(\"reveal\"), greetingHold + 220);\n return () => window.clearTimeout(t);\n }\n const t = window.setTimeout(() => setIndex((i) => i + 1), greetingHold);\n return () => window.clearTimeout(t);\n }, [phase, index, greetingHold, greetings.length]);\n\n // Drive the curtain reveal.\n React.useEffect(() => {\n if (phase !== \"reveal\") return;\n const controls = animate(progress, 1, {\n duration: revealDuration / 1000,\n ease: [0.85, 0, 0.15, 1],\n onComplete: () => {\n if (storageKey && typeof window !== \"undefined\") {\n try {\n window.sessionStorage.setItem(storageKey, \"done\");\n } catch {\n /* ignore */\n }\n }\n setPhase(\"done\");\n },\n });\n return () => controls.stop();\n }, [phase, progress, revealDuration, storageKey]);\n\n const showOverlay = phase !== \"done\";\n const current = greetings[Math.min(index, greetings.length - 1)];\n\n return (\n \n
{children}
\n\n \n {showOverlay && (\n \n {/* Cycled greeting */}\n
\n \n {phase === \"intro\" && current && (\n \n {current.text}\n \n )}\n \n
\n\n {/* Rising curved curtain */}\n \n \n \n \n )}\n
\n
\n );\n}\n\nexport default ArcRevealHero;\n", "type": "registry:ui", "target": "components/ruixen/arc-reveal-hero.tsx" } ] }