diff --git a/apps/web/src/discovery/DiscoveryPage.tsx b/apps/web/src/discovery/DiscoveryPage.tsx index 0939fb7..7892283 100644 --- a/apps/web/src/discovery/DiscoveryPage.tsx +++ b/apps/web/src/discovery/DiscoveryPage.tsx @@ -1,13 +1,35 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Canvas, useFrame } from "@react-three/fiber"; import * as THREE from "three"; -import { Avatar } from "@ourspace/avatar"; +import { Avatar, type AvatarMotion } from "@ourspace/avatar"; import { CRAFTS, lerp, smoothstep, type ProfileDoc } from "@ourspace/core"; import { AvatarStage } from "../components/AvatarStage"; import { useApp, type DiscoveryMode } from "../store"; import { DISCOVERY_CREATORS, sharedCraftCount, type DiscoveryCreator } from "./catalog"; +import { Spaceship } from "./Spaceship"; -type FlightPhase = "run" | "drop" | "dock" | "arrived"; +type FlightPhase = "depart" | "sprint" | "leap" | "fall" | "catch" | "merge" | "arrived"; + +const ENTRY_HOLD = 0.72; +const RUN_END = 2.65; +const LEAP_END = 3.5; +const FALL_END = 5.85; +const CATCH_END = 7.2; +const MERGE_END = 9.85; + +function segment(time: number, start: number, end: number): number { + return smoothstep(start, end, time); +} + +function phaseAt(time: number): FlightPhase { + if (time < 0.38) return "depart"; + if (time < RUN_END) return "sprint"; + if (time < LEAP_END) return "leap"; + if (time < FALL_END) return "fall"; + if (time < CATCH_END) return "catch"; + if (time < MERGE_END) return "merge"; + return "arrived"; +} const MODES: Array<{ id: DiscoveryMode; label: string; mark: string }> = [ { id: "all", label: "All worlds", mark: "∞" }, @@ -37,9 +59,10 @@ function seededOrder(value: string): number { } function StarField() { + const field = useRef(null); const points = useMemo(() => { - const values = new Float32Array(220 * 3); - for (let index = 0; index < 220; index++) { + const values = new Float32Array(300 * 3); + for (let index = 0; index < 300; index++) { const a = Math.sin(index * 12.9898) * 43758.5453; const b = Math.sin(index * 78.233 + 4.1) * 24634.6345; const c = Math.sin(index * 39.425 + 9.7) * 13579.2468; @@ -50,16 +73,67 @@ function StarField() { return values; }, []); + useFrame((state, delta) => { + if (!field.current) return; + field.current.rotation.z += delta * 0.003; + field.current.rotation.y = Math.sin(state.clock.elapsedTime * 0.07) * 0.018; + }); + return ( - + - + ); } +function SpeedLines({ + groupRef, + materialRef, +}: { + groupRef: React.RefObject; + materialRef: React.RefObject; +}) { + const lines = useMemo(() => { + const values = new Float32Array(72 * 6); + for (let index = 0; index < 72; index++) { + const a = Math.sin(index * 91.73) * 31821.771; + const b = Math.sin(index * 37.19 + 2.7) * 18731.227; + const x = (a - Math.floor(a) - 0.5) * 12; + const y = (b - Math.floor(b) - 0.5) * 8; + const z = -1.2 - (index % 12) * 0.72; + const offset = index * 6; + values[offset] = x; + values[offset + 1] = y; + values[offset + 2] = z; + values[offset + 3] = x * 1.02; + values[offset + 4] = y * 1.02; + values[offset + 5] = z - 0.75 - (index % 4) * 0.32; + } + return values; + }, []); + + return ( + + + + + + + + + ); +} + function MoodboardLedge() { return ( @@ -67,83 +141,37 @@ function MoodboardLedge() { - {[-2.2, 0, 2.2].map((x, index) => ( + {[-2.55, -0.85, 0.85, 2.55].map((x, index) => ( - + ))} - - + + - - ); -} - -function Spaceship({ - shipRef, - exteriorRef, -}: { - shipRef: React.RefObject; - exteriorRef: React.RefObject; -}) { - return ( - - - - - - - - - - - - - - - - - - - - - {/* Cockpit elements remain visible when the camera moves inside. */} - - - - - - - - - - - - - - - - - {[-1.4, -0.85, -0.3, 0.3, 0.85, 1.4].map((x, index) => ( - - - ( + + + ))} + ); } @@ -224,6 +252,7 @@ function Planet({ function FlightScene({ profile, skipIntro, + avatarMotion, creators, selectedId, onSelect, @@ -231,6 +260,7 @@ function FlightScene({ }: { profile: ProfileDoc; skipIntro: boolean; + avatarMotion: AvatarMotion; creators: DiscoveryCreator[]; selectedId: string; onSelect: (id: string) => void; @@ -239,111 +269,211 @@ function FlightScene({ const avatar = useRef(null); const deck = useRef(null); const ship = useRef(null); - const shipExterior = useRef(null); + const canopyLeft = useRef(null); + const canopyRight = useRef(null); + const speedLines = useRef(null); + const speedMaterial = useRef(null); + const planetField = useRef(null); const start = useRef(null); const lastPhase = useRef(null); - const lookAt = useMemo(() => new THREE.Vector3(), []); + const currentLook = useMemo(() => new THREE.Vector3(0, 0.9, 0), []); + const desiredPosition = useMemo(() => new THREE.Vector3(), []); + const desiredLook = useMemo(() => new THREE.Vector3(), []); + const cameraOffset = useMemo(() => new THREE.Vector3(), []); - useFrame((state) => { + useFrame((state, delta) => { if (start.current === null) start.current = state.clock.elapsedTime; - const elapsed = skipIntro ? 99 : state.clock.elapsedTime - start.current; + const elapsed = skipIntro + ? MERGE_END + 1 + : Math.max(0, state.clock.elapsedTime - start.current - ENTRY_HOLD); const avatarGroup = avatar.current; const shipGroup = ship.current; - const exteriorGroup = shipExterior.current; const deckGroup = deck.current; const camera = state.camera; - - let phase: FlightPhase; - if (elapsed < 2.35) phase = "run"; - else if (elapsed < 4.7) phase = "drop"; - else if (elapsed < 7.4) phase = "dock"; - else phase = "arrived"; + const phase = phaseAt(elapsed); if (phase !== lastPhase.current) { lastPhase.current = phase; onPhase(phase); } - if (!avatarGroup || !shipGroup || !deckGroup || !exteriorGroup) return; + if (!avatarGroup || !shipGroup || !deckGroup || !canopyLeft.current || !canopyRight.current) + return; + + let shipY = -10; + if (elapsed >= 4.1 && elapsed < FALL_END) { + shipY = lerp(-10, -5.6, segment(elapsed, 4.1, FALL_END)); + } else if (elapsed >= FALL_END && elapsed < CATCH_END) { + shipY = lerp(-5.6, -2.5, segment(elapsed, FALL_END, CATCH_END)); + } else if (elapsed >= CATCH_END && elapsed < MERGE_END) { + shipY = lerp(-2.5, 0, segment(elapsed, CATCH_END, MERGE_END)); + } else if (elapsed >= MERGE_END) { + shipY = 0; + } + shipGroup.position.set(0, shipY, 0); + shipGroup.rotation.x = + elapsed < FALL_END ? lerp(-0.08, 0.02, segment(elapsed, 4.1, FALL_END)) : 0; + shipGroup.rotation.z = + elapsed < CATCH_END + ? Math.sin(segment(elapsed, 4.1, CATCH_END) * Math.PI) * -0.075 + : THREE.MathUtils.damp(shipGroup.rotation.z, 0, 5, delta); + + const canopyClose = segment(elapsed, CATCH_END + 0.2, MERGE_END - 0.35); + canopyLeft.current.rotation.z = lerp(-0.92, 0, canopyClose); + canopyRight.current.rotation.z = lerp(0.92, 0, canopyClose); + + const deckExit = segment(elapsed, LEAP_END - 0.15, FALL_END - 0.2); + deckGroup.position.set(0, lerp(0, 8.5, deckExit), lerp(0, 2.6, deckExit)); + deckGroup.rotation.z = deckExit * -0.08; - if (phase === "run") { - const progress = smoothstep(0, 2.35, elapsed); + if (planetField.current) { + const reveal = segment(elapsed, MERGE_END - 1.25, MERGE_END); + planetField.current.visible = reveal > 0.005; + planetField.current.scale.setScalar(Math.max(0.001, reveal)); + planetField.current.position.z = lerp(-7, 0, reveal); + } + + if (phase === "depart" || phase === "sprint") { + const progress = segment(elapsed, 0.22, RUN_END); + avatarGroup.visible = true; + avatarGroup.scale.setScalar(1); + avatarGroup.position.set(lerp(-3.25, 2.82, progress), 0.2, 0.35); + avatarGroup.rotation.set(0, Math.PI / 2, 0); + } else if (phase === "leap") { + const progress = segment(elapsed, RUN_END, LEAP_END); avatarGroup.visible = true; + avatarGroup.scale.setScalar(1); avatarGroup.position.set( - lerp(-2.4, 2.55, progress), - 0.2 + Math.sin(progress * Math.PI) * 0.12, - 0.4, + lerp(2.82, 3.55, progress), + 0.2 + Math.sin(progress * Math.PI) * 0.82 - progress * progress * 0.3, + lerp(0.35, 0.25, progress), ); - avatarGroup.rotation.set(0, Math.PI / 2, 0); - deckGroup.position.y = 0; - shipGroup.position.y = -5.2; - exteriorGroup.visible = true; - camera.position.set(lerp(-0.7, 0.55, progress), 2.5, lerp(7.2, 5.9, progress)); - lookAt.set(avatarGroup.position.x * 0.24, 0.9, 0.1); - camera.lookAt(lookAt); - } else if (phase === "drop") { - const progress = smoothstep(2.35, 4.7, elapsed); + avatarGroup.rotation.set(0.04, Math.PI / 2, -Math.sin(progress * Math.PI) * 0.24); + } else if (phase === "fall") { + const progress = segment(elapsed, LEAP_END, FALL_END); avatarGroup.visible = true; + avatarGroup.scale.setScalar(1); avatarGroup.position.set( - 2.55 - progress * 1.6, - lerp(0.2, -4.1, progress * progress), - lerp(0.4, 0, progress), + lerp(3.55, 0, progress), + lerp(-0.1, -5.25, progress * progress), + lerp(0.25, 3.08, progress), ); - avatarGroup.rotation.set(progress * 1.6, Math.PI / 2 + progress * 2.2, progress * 0.45); - deckGroup.position.y = lerp(0, 4.5, progress); - shipGroup.position.y = -5.2; - exteriorGroup.visible = true; - camera.position.set( - lerp(0.55, 0.15, progress), - lerp(2.5, -1.2, progress), - lerp(5.9, 6.7, progress), + avatarGroup.rotation.set( + lerp(0.04, 0.18, progress), + lerp(Math.PI / 2, Math.PI, progress), + Math.sin(progress * Math.PI) * 0.16, ); - lookAt.copy(avatarGroup.position); - camera.lookAt(lookAt); - } else if (phase === "dock") { - const progress = smoothstep(4.7, 7.4, elapsed); - avatarGroup.visible = progress < 0.22; - avatarGroup.position.y = -4.1; - deckGroup.position.y = 4.5; - shipGroup.position.y = lerp(-5.2, 0, progress); - exteriorGroup.visible = progress < 0.78; - camera.position.set(0, lerp(-1.2, 1.48, progress), lerp(6.7, 4.75, progress)); - lookAt.set(0, lerp(-3.8, 0.8, progress), lerp(0, -8.5, progress)); - camera.lookAt(lookAt); + } else if (phase === "catch" || phase === "merge") { + const mergeProgress = segment(elapsed, CATCH_END, MERGE_END); + avatarGroup.visible = phase === "catch" || mergeProgress < 0.54; + avatarGroup.scale.setScalar(1); + avatarGroup.position.set(0, shipY + 0.35, 3.08); + avatarGroup.rotation.set(0, Math.PI, 0); } else { avatarGroup.visible = false; - deckGroup.position.y = 4.5; - shipGroup.position.y = 0; - exteriorGroup.visible = false; + } + + let targetFov = 48; + let roll = 0; + if (phase === "depart" || phase === "sprint") { + const progress = segment(elapsed, 0, RUN_END); + desiredPosition.set( + lerp(-0.85, 1.46, progress), + lerp(2.68, 2.18, progress), + lerp(7.75, 6.15, progress), + ); + desiredLook.set(lerp(-1.2, 2.65, progress), lerp(0.88, 0.72, progress), 0.3); + targetFov = lerp(47, 51, progress); + roll = Math.sin(progress * Math.PI) * -0.018; + } else if (phase === "leap") { + const progress = segment(elapsed, RUN_END, LEAP_END); + desiredPosition + .copy(avatarGroup.position) + .add(cameraOffset.set(lerp(-1.18, -0.92, progress), lerp(1.65, 1.45, progress), 5.62)); + desiredLook.copy(avatarGroup.position).add(cameraOffset.set(0, 0.72, 0)); + targetFov = lerp(51, 57, progress); + roll = lerp(-0.018, -0.055, progress); + } else if (phase === "fall") { + const progress = segment(elapsed, LEAP_END, FALL_END); + desiredPosition + .copy(avatarGroup.position) + .add(cameraOffset.set(lerp(-0.92, 0.72, progress), lerp(1.45, 0.72, progress), 5.45)); + desiredLook.copy(avatarGroup.position).add(cameraOffset.set(0, 0.62, 0)); + targetFov = 57 + Math.sin(progress * Math.PI) * 5; + roll = lerp(-0.055, 0.035, progress); + } else if (phase === "catch") { + const progress = segment(elapsed, FALL_END, CATCH_END); + desiredPosition.set( + lerp(0.72, 0, progress), + shipY + lerp(1.08, 2.28, progress), + lerp(8.48, 6.35, progress), + ); + desiredLook.copy(avatarGroup.position).add(cameraOffset.set(0, 0.9, 0)); + targetFov = lerp(58, 52, progress); + roll = Math.sin(progress * Math.PI) * 0.045; + } else if (phase === "merge") { + const progress = segment(elapsed, CATCH_END, MERGE_END); + desiredPosition.set(0, shipY + lerp(2.28, 1.7, progress), lerp(6.35, 2.86, progress)); + const forwardBlend = segment(progress, 0.38, 0.82); + desiredLook.set(0, lerp(shipY + 1.92, 0.72, forwardBlend), lerp(3.0, -10.2, forwardBlend)); + targetFov = lerp(52, 48, progress); + roll = Math.sin(progress * Math.PI * 2) * 0.018 * (1 - progress); + } else { const time = state.clock.elapsedTime; - camera.position.set( - Math.sin(time * 0.35) * 0.045, - 1.48 + Math.sin(time * 0.48) * 0.025, - 4.75, + desiredPosition.set(Math.sin(time * 0.34) * 0.035, 1.7 + Math.sin(time * 0.46) * 0.018, 2.86); + desiredLook.set(Math.sin(time * 0.22) * 0.03, 0.72, -10.2); + } + + const cameraEase = 1 - Math.exp(-delta * (phase === "arrived" ? 4.2 : 8.5)); + camera.position.lerp(desiredPosition, cameraEase); + currentLook.lerp(desiredLook, 1 - Math.exp(-delta * 7.5)); + camera.up.set(0, 1, 0); + camera.lookAt(currentLook); + camera.rotateZ(roll); + if (camera instanceof THREE.PerspectiveCamera) { + camera.fov = THREE.MathUtils.damp(camera.fov, targetFov, 7, delta); + camera.updateProjectionMatrix(); + } + + if (speedLines.current && speedMaterial.current) { + speedLines.current.position.copy(camera.position); + speedLines.current.quaternion.copy(camera.quaternion); + speedLines.current.translateZ((state.clock.elapsedTime * 7.5) % 1.15); + const speedTarget = + phase === "fall" + ? Math.sin(segment(elapsed, LEAP_END, FALL_END) * Math.PI) * 0.6 + : phase === "catch" + ? 0.24 * (1 - segment(elapsed, FALL_END, CATCH_END)) + : 0; + speedMaterial.current.opacity = THREE.MathUtils.damp( + speedMaterial.current.opacity, + speedTarget, + 7, + delta, ); - lookAt.set(0, 0.55, -10.2); - camera.lookAt(lookAt); } }); return ( <> - - - + + + + + - - + + - + - + {creators.map((creator, index) => ( state.discovery.randomSeed); const selectedId = useApp((state) => state.discovery.selectedId); const updateDiscovery = useApp((state) => state.updateDiscovery); - const [phase, setPhase] = useState(resume ? "arrived" : "run"); + const [reducedMotion] = useState( + () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false, + ); + const skipSequence = resume || reducedMotion; + const [phase, setPhase] = useState(skipSequence ? "arrived" : "depart"); const setSelectedId = useCallback( (id: string) => updateDiscovery({ selectedId: id }), [updateDiscovery], @@ -419,6 +553,16 @@ export function DiscoveryPage({ resume = false }: { resume?: boolean }) { const selected = creators.find((creator) => creator.id === selectedId) ?? creators[0]; const arrived = phase === "arrived"; + const avatarMotion: AvatarMotion = + phase === "depart" || phase === "sprint" + ? "run" + : phase === "leap" + ? "jump" + : phase === "fall" + ? "fall" + : phase === "catch" || phase === "merge" + ? "land" + : "idle"; const selectRelative = useCallback( (direction: -1 | 1) => { if (!creators.length) return; @@ -455,13 +599,18 @@ export function DiscoveryPage({ resume = false }: { resume?: boolean }) { return (
{ + gl.toneMapping = THREE.ACESFilmicToneMapping; + gl.toneMappingExposure = 1.06; + }} >
+
diff --git a/apps/web/src/discovery/Spaceship.tsx b/apps/web/src/discovery/Spaceship.tsx new file mode 100644 index 0000000..20889e0 --- /dev/null +++ b/apps/web/src/discovery/Spaceship.tsx @@ -0,0 +1,249 @@ +import { useMemo, type RefObject } from "react"; +import * as THREE from "three"; + +interface SpaceshipProps { + rootRef: RefObject; + canopyLeftRef: RefObject; + canopyRightRef: RefObject; +} + +function Wing({ side }: { side: -1 | 1 }) { + const shape = useMemo(() => { + const value = new THREE.Shape(); + value.moveTo(0.55, -1.35); + value.lineTo(3.45, -0.25); + value.lineTo(3.05, 1.05); + value.lineTo(0.45, 0.58); + value.closePath(); + return value; + }, []); + + return ( + + + + + ); +} + +function ConsoleLights() { + const colors = ["#54ffdc", "#ff4dd8", "#c8ff32", "#65a7ff"]; + return ( + <> + {Array.from({ length: 10 }, (_, index) => { + const row = Math.floor(index / 5); + const column = index % 5; + return ( + + + + + ); + })} + + ); +} + +/** + * Runtime-built courier craft: broad PS2-era silhouette outside, readable + * cockpit framing inside. The camera enters through the open rear canopy, so + * no hull mesh needs to pop out of existence during the POV handoff. + */ +export function Spaceship({ rootRef, canopyLeftRef, canopyRightRef }: SpaceshipProps) { + return ( + + + + + + + + + + + + + + + + + + + + {([-1, 1] as const).map((side) => ( + + + + + + + + + + + + ))} + + + + + + + + + + + + + + + + + + + + + + + + {([-1, 1] as const).map((side) => ( + + + + + + + + + + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index ce31ad1..42277d6 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1948,6 +1948,8 @@ audio { radial-gradient(circle at 8% 50%, rgba(84, 255, 220, 0.15), transparent 21%), radial-gradient(circle at 82% 10%, rgba(255, 77, 216, 0.12), transparent 30%), linear-gradient(145deg, #161525, #090a11); + view-transition-name: orbit-launch; + contain: paint; } .orbit-launch-card::after { content: ""; @@ -2018,6 +2020,12 @@ audio { min-height: 50px; white-space: nowrap; } +.launch-orbit-button:hover { + box-shadow: + 0 0 0 1px rgba(84, 255, 220, 0.36), + 0 0 28px rgba(84, 255, 220, 0.22); + transform: translateY(-2px); +} .launch-orbit-hint { position: absolute; right: 27px; @@ -2035,6 +2043,19 @@ audio { inset: 0; overflow: hidden; background: #030308; + view-transition-name: orbit-launch; + contain: paint; +} +.discovery::after { + content: ""; + position: absolute; + inset: 0; + z-index: 2; + pointer-events: none; + background: + repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.018) 0 1px, transparent 1px 4px), + radial-gradient(circle at center, transparent 42%, rgba(1, 2, 7, 0.36) 100%); + mix-blend-mode: screen; } .discovery canvas { display: block; @@ -2044,6 +2065,126 @@ audio { inset: 0; pointer-events: none; } +.flight-cinematic-frame { + position: absolute; + inset: 76px 28px 28px; + z-index: 2; + opacity: 0.72; + pointer-events: none; + transition: opacity 900ms ease; +} +.arrived .flight-cinematic-frame { + opacity: 0; +} +.flight-cinematic-frame::before, +.flight-cinematic-frame::after { + content: ""; + position: absolute; + width: 58px; + height: 58px; + border-color: rgba(84, 255, 220, 0.54); + border-style: solid; +} +.flight-cinematic-frame::before { + top: 0; + left: 0; + border-width: 1px 0 0 1px; +} +.flight-cinematic-frame::after { + right: 0; + bottom: 0; + border-width: 0 1px 1px 0; +} +.flight-cinematic-frame > i { + position: absolute; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 77, 216, 0.22), transparent); +} +.flight-cinematic-frame > i:first-child { + top: 32%; +} +.flight-cinematic-frame > i:nth-child(2) { + top: 68%; +} +.flight-cinematic-frame > span { + position: absolute; + left: 8px; + bottom: 8px; + color: rgba(200, 255, 50, 0.72); + font: + 700 0.48rem "Courier New", + monospace; + letter-spacing: 0.14em; +} +.flight-cinematic-frame > b { + position: absolute; + top: 50%; + left: 50%; + color: rgba(84, 255, 220, 0.52); + font: 400 1.35rem monospace; + transform: translate(-50%, -50%); + animation: flight-reticle 1.8s ease-in-out infinite; +} +@keyframes flight-reticle { + 50% { + transform: translate(-50%, -50%) scale(1.25) rotate(45deg); + color: rgba(255, 77, 216, 0.62); + } +} + +::view-transition-group(orbit-launch) { + z-index: 100; + overflow: hidden; + animation-duration: 920ms; + animation-timing-function: cubic-bezier(0.2, 0.78, 0.16, 1); +} +::view-transition-old(orbit-launch) { + animation: orbit-card-depart 920ms cubic-bezier(0.2, 0.78, 0.16, 1) both; +} +::view-transition-new(orbit-launch) { + z-index: 2; + animation: orbit-world-arrive 920ms cubic-bezier(0.2, 0.78, 0.16, 1) both; + mix-blend-mode: normal; +} +::view-transition-old(root) { + animation: orbit-page-recede 920ms cubic-bezier(0.2, 0.78, 0.16, 1) both; +} +::view-transition-new(root) { + animation: none; +} +@keyframes orbit-card-depart { + 0%, + 62% { + opacity: 1; + filter: saturate(1) brightness(1); + } + 100% { + opacity: 0; + filter: saturate(1.5) brightness(1.8); + } +} +@keyframes orbit-world-arrive { + 0% { + opacity: 0.45; + filter: saturate(1.4) brightness(1.35); + } + 48% { + opacity: 0.75; + } + 100% { + opacity: 1; + filter: none; + } +} +@keyframes orbit-page-recede { + to { + opacity: 0; + transform: scale(0.965) translateY(-18px); + filter: blur(5px) saturate(0.7); + } +} .flight-status { position: absolute; top: 16px; diff --git a/apps/web/src/profile/ProfilePage.tsx b/apps/web/src/profile/ProfilePage.tsx index 0d4ef83..a2218e1 100644 --- a/apps/web/src/profile/ProfilePage.tsx +++ b/apps/web/src/profile/ProfilePage.tsx @@ -106,6 +106,17 @@ export function ProfilePage() { removeModule(module.id); }; + const launchIntoOrbit = () => { + const navigate = () => setScreen({ name: "discovery" }); + const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; + const startViewTransition = document.startViewTransition?.bind(document); + if (!prefersReducedMotion && startViewTransition) { + startViewTransition(navigate); + return; + } + navigate(); + }; + return (