diff --git a/public/models/fox.glb b/public/models/fox.glb
new file mode 100644
index 00000000..e36eac43
Binary files /dev/null and b/public/models/fox.glb differ
diff --git a/src/assets/models/fox.glb b/src/assets/models/fox.glb
new file mode 100644
index 00000000..e36eac43
Binary files /dev/null and b/src/assets/models/fox.glb differ
diff --git a/src/components/FoxMascot/FoxMascot.jsx b/src/components/FoxMascot/FoxMascot.jsx
new file mode 100644
index 00000000..250270da
--- /dev/null
+++ b/src/components/FoxMascot/FoxMascot.jsx
@@ -0,0 +1,309 @@
+/**
+ * @fileoverview FoxMascot Component - Main canvas wrapper for 3D fox animation
+ * @module components/FoxMascot/FoxMascot
+ */
+
+import React, { useRef, useEffect, useState, useCallback, Suspense } from 'react';
+import { Canvas } from '@react-three/fiber';
+import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
+import * as THREE from 'three';
+import FoxModel from './FoxModel';
+import useAnimationSequence from './useAnimationSequence';
+import styles from './FoxMascot.module.scss';
+
+/**
+ * FoxMascot Component - Renders the 3D fox mascot animation overlay
+ * @param {Object} props
+ * @param {Function} props.onComplete - Called when animation finishes or is skipped
+ * @param {boolean} props.isVisible - Controls visibility of the component
+ */
+const FoxMascot = ({ onComplete, isVisible = true }) => {
+ const foxRef = useRef();
+ const canvasRef = useRef();
+ const [showSkip, setShowSkip] = useState(true);
+ const [currentPhase, setCurrentPhase] = useState(null);
+ const [isLoaded, setIsLoaded] = useState(false);
+
+ /**
+ * Handle phase change from animation sequence
+ */
+ const handlePhaseChange = useCallback((phase) => {
+ setCurrentPhase(phase);
+ console.log(`[FoxMascot] Phase: ${phase}`);
+ }, []);
+
+ /**
+ * Handle animation complete
+ */
+ const handleComplete = useCallback(() => {
+ console.log('[FoxMascot] Animation complete');
+ setShowSkip(false);
+ if (onComplete) {
+ onComplete();
+ }
+ }, [onComplete]);
+
+ // Animation sequence hook
+ const animation = useAnimationSequence(foxRef, handleComplete, handlePhaseChange);
+
+ /**
+ * Start animation when fox model is loaded
+ * Sequence: Jump → Run (move left) → Walk (continue left) → Sit
+ * Combines skeletal animations with position movement
+ */
+ /**
+ * AVAILABLE FOX ANIMATIONS:
+ * -------------------------
+ * Fox_Attack_Paws, Fox_Attack_Tail, Fox_Falling, Fox_Falling_Left,
+ * Fox_Idle, Fox_Jump, Fox_Jump_InAir, Fox_Jump_Pivot_InPlace,
+ * Fox_Run, Fox_Run_InPlace, Fox_Run_Left, Fox_Run_Left_InPlace,
+ * Fox_Run_Right, Fox_Run_Right_InPlace, Fox_Sit1, Fox_Sit2_Idle,
+ * Fox_Sit3_StandUp, Fox_Sit_Idle_Break, Fox_Sit_No, Fox_Sit_Yes,
+ * Fox_Somersault, Fox_Somersault_InPlace, Fox_Walk, Fox_Walk_Back,
+ * Fox_Walk_Back_InPlace, Fox_Walk_InPlace, Fox_Walk_Left,
+ * Fox_Walk_Left_InPlace, Fox_Walk_Right, Fox_Walk_Right_InPlace
+ */
+ const handleFoxLoaded = useCallback(() => {
+ setIsLoaded(true);
+ console.log('[FoxMascot] Fox loaded - IN-PLACE ANIMATION with GSAP movement');
+
+ if (!foxRef.current) return;
+
+ const fox = foxRef.current;
+ const group = fox.getGroup();
+
+ if (!group) {
+ console.error('[FoxMascot] Could not get group for position animation');
+ return;
+ }
+
+ // Animation durations (seconds)
+ const JUMP_DURATION = 1.2;
+ const RUN_DURATION = 1.5;
+ const FALL_DURATION = 1.2; // Falling animation
+ const IDLE_DURATION = 1.0; // Stand up / recover
+ const TURN_DURATION = 0.8; // Turn to face user
+ const WALK_DURATION = 2.0; // Walk to final position
+ const SIT_DURATION = 0.53;
+
+ const crossfade = 0.3; // Smooth crossfade between animations
+
+ // Starting position (right side of screen)
+ const START_X = 55;
+ const END_X = -20; // Final position (left side)
+
+ // Set initial position
+ group.position.x = START_X;
+ group.position.y = -35;
+
+ console.log('[FoxMascot] Animation sequence:');
+ console.log(' 1. Jump → 2. Run → 3. Fall');
+ console.log(' 4. Idle (recover) → 5. Turn to face user');
+ console.log(' 6. Walk → 7. Sit');
+
+ // Import GSAP and create timeline
+ import('gsap').then(({ default: gsap }) => {
+
+ // Calculate movement distances
+ const jumpDistance = 5;
+ const runDistance = 30;
+ const fallDistance = 3; // Slight forward during fall
+ const walkDistance = 37; // Walk to final position
+
+ // Position checkpoints
+ const afterJumpX = START_X - jumpDistance; // 50
+ const afterRunX = afterJumpX - runDistance; // 20
+ const afterFallX = afterRunX - fallDistance; // 17
+ const afterWalkX = afterFallX - walkDistance; // -20
+
+ // Create GSAP timeline
+ const tl = gsap.timeline({
+ onComplete: () => {
+ console.log('[FoxMascot] ✓ Animation sequence complete!');
+ console.log(`[FoxMascot] Final position: ${group.position.x.toFixed(2)}`);
+ }
+ });
+
+ // ===== PHASE 1: JUMP =====
+ tl.add(() => {
+ fox.playAnimation('Fox_Jump', 0, false, 1);
+ console.log('[FoxMascot] ▶ Phase 1: JUMP');
+ })
+ .to(group.position, {
+ x: afterJumpX,
+ duration: JUMP_DURATION,
+ ease: 'power2.out'
+ }, '<')
+
+ // ===== PHASE 2: RUN =====
+ .add(() => {
+ fox.playAnimation('Fox_Run_InPlace', crossfade, true, 1);
+ console.log('[FoxMascot] ▶ Phase 2: RUN');
+ })
+ .to(group.position, {
+ x: afterRunX,
+ duration: RUN_DURATION,
+ ease: 'linear'
+ }, '<')
+
+ // ===== PHASE 3: FALL (cute stumble) =====
+ .add(() => {
+ fox.playAnimation('Fox_Falling', crossfade, false, 1);
+ console.log('[FoxMascot] ▶ Phase 3: FALL (oops!)');
+ })
+ .to(group.position, {
+ x: afterFallX,
+ duration: FALL_DURATION,
+ ease: 'power2.out'
+ }, '<')
+
+ // ===== PHASE 4: IDLE (recover/stand up) =====
+ .add(() => {
+ fox.playAnimation('Fox_Idle', crossfade, true, 1);
+ console.log('[FoxMascot] ▶ Phase 4: IDLE (recovering)');
+ })
+ .to({}, { duration: IDLE_DURATION })
+
+ // ===== PHASE 5: TURN HEAD TO FACE USER =====
+ // Rotate only the head bone while staying in Idle (no leg/body movement)
+ .add(() => {
+ console.log('[FoxMascot] ▶ Phase 5: TURNING HEAD TO FACE USER');
+ fox.setHeadRotation(0, true); // Enable head rotation
+ })
+ .to({ headX: 0 }, {
+ headX: 1, // Turn head LEFT to look at user
+ duration: TURN_DURATION,
+ ease: 'power2.inOut',
+ onUpdate: function () {
+ const xVal = this.targets()[0].headX;
+ fox.setHeadRotation(xVal, true);
+ }
+ })
+
+ // ===== PHASE 5b: TURN HEAD BACK TO ORIGINAL =====
+ .add(() => {
+ console.log('[FoxMascot] ▶ Phase 5b: TURNING HEAD BACK');
+ })
+ .to({ headX: 1 }, {
+ headX: 0, // Turn head back to original direction
+ duration: TURN_DURATION * 0.7,
+ ease: 'power2.inOut',
+ onUpdate: function () {
+ const xVal = this.targets()[0].headX;
+ fox.setHeadRotation(xVal, true);
+ }
+ })
+
+ // ===== PHASE 6: WALK =====
+ .add(() => {
+ fox.setHeadRotation(0, false); // Disable head override
+ fox.playAnimation('Fox_Walk_InPlace', crossfade, true, 1);
+ console.log('[FoxMascot] ▶ Phase 6: WALK');
+ })
+ .to(group.position, {
+ x: afterWalkX,
+ duration: WALK_DURATION,
+ ease: 'linear'
+ }, '<')
+
+ // ===== PHASE 7: TURN TO FACE USER =====
+ .add(() => {
+ fox.playAnimation('Fox_Walk_Left_InPlace', crossfade, true, 1);
+ console.log('[FoxMascot] ▶ Phase 7: TURN TO FACE USER');
+ })
+ .to(group.rotation, {
+ y: Math.PI / 12, // Rotate slightly to face camera
+ duration: 1.0,
+ ease: 'power2.inOut'
+ }, '<')
+
+ // ===== PHASE 8: SIT (facing user) =====
+ .add(() => {
+ fox.playAnimation('Fox_Sit2_Idle', crossfade, true, 1); // Use idle sit - more forward looking
+ console.log('[FoxMascot] ▶ Phase 8: SIT (facing user)');
+ })
+ // Tilt fox upward so it looks at the screen/user
+ .to(group.rotation, {
+ x: -0.30, // Tilt more to look at user
+ duration: 0.5,
+ ease: 'power2.out'
+ }, '<+0.3');
+
+ });
+
+ }, []);
+
+ /**
+ * Handle skip button click
+ */
+ const handleSkip = useCallback(() => {
+ animation.skip();
+ }, [animation]);
+
+ // DISABLED: Start animation once loaded
+ // useEffect(() => {
+ // if (isLoaded && foxRef.current && isVisible) {
+ // animation.play();
+ // }
+ // }, [isLoaded, isVisible]);
+
+ if (!isVisible) {
+ return null;
+ }
+
+ return (
+
+ {/* Skip Button */}
+ {showSkip && (
+
+ )}
+
+ {/* Three.js Canvas */}
+
+
+ );
+};
+
+export default FoxMascot;
diff --git a/src/components/FoxMascot/FoxMascot.module.scss b/src/components/FoxMascot/FoxMascot.module.scss
new file mode 100644
index 00000000..34ed3f58
--- /dev/null
+++ b/src/components/FoxMascot/FoxMascot.module.scss
@@ -0,0 +1,62 @@
+/**
+ * FoxMascot Styles
+ * Full-screen overlay for 3D fox mascot animation
+ */
+
+.foxMascotContainer {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ pointer-events: none;
+ z-index: 9998;
+ /* Below chatbot modal (9999), above everything else */
+ overflow: hidden;
+}
+
+.canvas {
+ width: 100%;
+ height: 100%;
+ background: transparent;
+}
+
+.skipButton {
+ position: fixed;
+ bottom: 100px;
+ right: 20px;
+ padding: 10px 20px;
+ background: rgba(255, 255, 255, 0.9);
+ border: 2px solid #ff6b35;
+ border-radius: 25px;
+ color: #ff6b35;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ pointer-events: auto;
+ z-index: 10000;
+ transition: all 0.2s ease;
+ backdrop-filter: blur(5px);
+ box-shadow: 0 4px 15px rgba(255, 107, 53, 0.3);
+
+ &:hover {
+ background: #ff6b35;
+ color: white;
+ transform: scale(1.05);
+ box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4);
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+}
+
+/* Mobile adjustments */
+@media (max-width: 768px) {
+ .skipButton {
+ bottom: 80px;
+ right: 15px;
+ padding: 8px 16px;
+ font-size: 12px;
+ }
+}
\ No newline at end of file
diff --git a/src/components/FoxMascot/FoxModel.jsx b/src/components/FoxMascot/FoxModel.jsx
new file mode 100644
index 00000000..f8e3da5b
--- /dev/null
+++ b/src/components/FoxMascot/FoxModel.jsx
@@ -0,0 +1,370 @@
+/**
+ * @fileoverview FoxModel Component - Loads and animates the 3D fox mascot
+ * @module components/FoxMascot/FoxModel
+ *
+ * FIXES APPLIED:
+ * 1. Using Center from drei to auto-center the model geometry
+ * 2. Computing bounding box to understand model dimensions
+ * 3. Positioning the group AFTER centering
+ * 4. Removed cloning - using original scene with Center
+ */
+
+import React, { useRef, useEffect, useMemo, forwardRef, useImperativeHandle } from 'react';
+import { useGLTF, useAnimations, Center } from '@react-three/drei';
+import { useFrame } from '@react-three/fiber';
+import * as THREE from 'three';
+
+/**
+ * AVAILABLE FOX ANIMATIONS:
+ * -------------------------
+ * Fox_Attack_Paws - Attack with paws
+ * Fox_Attack_Tail - Attack with tail
+ * Fox_Falling - Falling animation
+ * Fox_Falling_Left - Falling to the left
+ * Fox_Idle - Standard idle/breathing
+ * Fox_Jump - Jump and land
+ * Fox_Jump_InAir - In-air jump pose
+ * Fox_Jump_Pivot_InPlace - Pivot jump in place
+ * Fox_Run - Running with root motion
+ * Fox_Run_InPlace - Running in place (treadmill)
+ * Fox_Run_Left - Running while turning left
+ * Fox_Run_Left_InPlace - Running left in place
+ * Fox_Run_Right - Running while turning right
+ * Fox_Run_Right_InPlace - Running right in place
+ * Fox_Sit1 - Transition from standing to sitting
+ * Fox_Sit2_Idle - Idle while sitting
+ * Fox_Sit3_StandUp - Standing up from sit
+ * Fox_Sit_Idle_Break - Idle break while sitting
+ * Fox_Sit_No - Shaking head no while sitting
+ * Fox_Sit_Yes - Nodding yes while sitting
+ * Fox_Somersault - Flip/tumble animation
+ * Fox_Somersault_InPlace - Somersault in place
+ * Fox_Walk - Walking with root motion
+ * Fox_Walk_Back - Walking backwards
+ * Fox_Walk_Back_InPlace - Walking backwards in place
+ * Fox_Walk_InPlace - Walking in place (treadmill)
+ * Fox_Walk_Left - Walking while turning left
+ * Fox_Walk_Left_InPlace - Walking left in place
+ * Fox_Walk_Right - Walking while turning right
+ * Fox_Walk_Right_InPlace - Walking right in place
+ */
+
+// Animation name constants
+const ANIMATIONS = {
+ // Basic
+ IDLE: 'Fox_Idle',
+ JUMP: 'Fox_Jump',
+ JUMP_INAIR: 'Fox_Jump_InAir',
+ JUMP_PIVOT_INPLACE: 'Fox_Jump_Pivot_InPlace',
+ FALLING: 'Fox_Falling',
+ FALLING_LEFT: 'Fox_Falling_Left',
+
+ // Attack
+ ATTACK_PAWS: 'Fox_Attack_Paws',
+ ATTACK_TAIL: 'Fox_Attack_Tail',
+
+ // Run
+ RUN: 'Fox_Run',
+ RUN_INPLACE: 'Fox_Run_InPlace',
+ RUN_LEFT: 'Fox_Run_Left',
+ RUN_LEFT_INPLACE: 'Fox_Run_Left_InPlace',
+ RUN_RIGHT: 'Fox_Run_Right',
+ RUN_RIGHT_INPLACE: 'Fox_Run_Right_InPlace',
+
+ // Walk
+ WALK: 'Fox_Walk',
+ WALK_INPLACE: 'Fox_Walk_InPlace',
+ WALK_LEFT: 'Fox_Walk_Left',
+ WALK_LEFT_INPLACE: 'Fox_Walk_Left_InPlace',
+ WALK_RIGHT: 'Fox_Walk_Right',
+ WALK_RIGHT_INPLACE: 'Fox_Walk_Right_InPlace',
+ WALK_BACK: 'Fox_Walk_Back',
+ WALK_BACK_INPLACE: 'Fox_Walk_Back_InPlace',
+
+ // Sit
+ SIT: 'Fox_Sit1',
+ SIT_IDLE: 'Fox_Sit2_Idle',
+ SIT_STANDUP: 'Fox_Sit3_StandUp',
+ SIT_IDLE_BREAK: 'Fox_Sit_Idle_Break',
+ SIT_NO: 'Fox_Sit_No',
+ SIT_YES: 'Fox_Sit_Yes',
+
+ // Tricks
+ SOMERSAULT: 'Fox_Somersault',
+ SOMERSAULT_INPLACE: 'Fox_Somersault_InPlace',
+};
+
+// Model path - must be in public folder for Vite
+const MODEL_PATH = '/models/fox.glb';
+
+/**
+ * FoxModel component - Loads GLB model and manages animations
+ * Uses Center from drei to properly center the model geometry
+ */
+const FoxModel = forwardRef(({ position = [0, 0, 0], rotation = [0, 0, 0], scale = 1, onLoaded }, ref) => {
+ const group = useRef();
+ const { scene, animations } = useGLTF(MODEL_PATH);
+ const { actions, mixer } = useAnimations(animations, group);
+
+ // Current playing animation tracking
+ const currentAnimation = useRef(null);
+
+ // Head rotation state (applied every frame after mixer update)
+ const headRotation = useRef({ y: 0, x: 0, enabled: false });
+ const headBoneRef = useRef(null);
+
+ // Compute bounding box and log model info on mount
+ useEffect(() => {
+ if (scene) {
+ // Compute bounding box to understand model size
+ const box = new THREE.Box3().setFromObject(scene);
+ const size = box.getSize(new THREE.Vector3());
+ const center = box.getCenter(new THREE.Vector3());
+
+ console.log('[FoxModel] === Model Info ===');
+ console.log('[FoxModel] Bounding box size:', size.x.toFixed(2), size.y.toFixed(2), size.z.toFixed(2));
+ console.log('[FoxModel] Bounding box center:', center.x.toFixed(2), center.y.toFixed(2), center.z.toFixed(2));
+ console.log('[FoxModel] Animations available:', Object.keys(actions));
+
+ // Log DURATION of each animation clip
+ console.log('[FoxModel] === Animation Durations ===');
+ animations.forEach(clip => {
+ console.log(`[FoxModel] ${clip.name}: ${clip.duration.toFixed(2)}s`);
+ });
+
+ // LOG ALL BONES in the model
+ console.log('[FoxModel] === ALL BONES ===');
+ const bones = [];
+ scene.traverse((child) => {
+ if (child.isBone) {
+ const worldPos = new THREE.Vector3();
+ child.getWorldPosition(worldPos);
+ bones.push({
+ name: child.name,
+ x: worldPos.x.toFixed(4),
+ y: worldPos.y.toFixed(4),
+ z: worldPos.z.toFixed(4)
+ });
+ console.log(`[FoxModel] BONE: "${child.name}" at (${worldPos.x.toFixed(4)}, ${worldPos.y.toFixed(4)}, ${worldPos.z.toFixed(4)})`);
+ }
+ });
+ console.log('[FoxModel] Total bones found:', bones.length);
+
+ console.log('[FoxModel] Props:', { position, rotation, scale });
+ }
+
+ if (onLoaded) {
+ onLoaded();
+ }
+ }, [scene, actions, animations, onLoaded, position, rotation, scale]);
+
+ /**
+ * Get the duration of an animation clip in seconds
+ */
+ const getAnimationDuration = (animationName) => {
+ const clip = animations.find(c => c.name === animationName);
+ return clip ? clip.duration : 0;
+ };
+
+ /**
+ * Crossfade to a new animation smoothly
+ * @param {string} animationName - Name of the animation to play
+ * @param {number} duration - Crossfade duration (default 0.5)
+ * @param {boolean|null} loop - Force loop on/off, or null for auto-detect
+ * @param {number} timeScale - Playback speed: 1=normal, 0.5=half speed, 2=double speed
+ */
+ const playAnimation = (animationName, duration = 0.5, loop = null, timeScale = 1) => {
+ const nextAction = actions[animationName];
+ if (!nextAction) {
+ console.warn(`[FoxModel] Animation "${animationName}" not found`);
+ return;
+ }
+
+ if (currentAnimation.current === animationName) {
+ return;
+ }
+
+ console.log(`[FoxModel] Playing animation: ${animationName} at speed ${timeScale}x`);
+
+ const prevAction = currentAnimation.current ? actions[currentAnimation.current] : null;
+
+ const shouldLoop = loop !== null ? loop :
+ animationName.includes('Walk') ||
+ animationName.includes('Run') ||
+ animationName.includes('Idle');
+
+ nextAction.reset();
+ nextAction.setLoop(shouldLoop ? THREE.LoopRepeat : THREE.LoopOnce, shouldLoop ? Infinity : 1);
+ nextAction.clampWhenFinished = !shouldLoop;
+ nextAction.setEffectiveWeight(1);
+ nextAction.setEffectiveTimeScale(timeScale); // Use the timeScale parameter
+ nextAction.play();
+
+ if (prevAction) {
+ prevAction.crossFadeTo(nextAction, duration, true);
+ }
+
+ currentAnimation.current = animationName;
+ };
+
+ /**
+ * Stop all animations
+ */
+ const stopAllAnimations = () => {
+ Object.values(actions).forEach(action => {
+ if (action) action.stop();
+ });
+ currentAnimation.current = null;
+ };
+
+ /**
+ * Get the world position of a bone (for tracking root motion)
+ * Returns the position of the root/hip bone or falls back to bounding box center
+ */
+ const getRootBonePosition = () => {
+ if (!scene) return new THREE.Vector3();
+
+ // Try to find root/hip bone
+ let rootBone = null;
+ scene.traverse((child) => {
+ if (child.isBone && (
+ child.name.toLowerCase().includes('root') ||
+ child.name.toLowerCase().includes('hip') ||
+ child.name.toLowerCase().includes('pelvis')
+ )) {
+ rootBone = child;
+ }
+ });
+
+ if (rootBone) {
+ const worldPos = new THREE.Vector3();
+ rootBone.getWorldPosition(worldPos);
+ return worldPos;
+ }
+
+ // Fallback: use bounding box center
+ const box = new THREE.Box3().setFromObject(scene);
+ return box.getCenter(new THREE.Vector3());
+ };
+
+ /**
+ * Get the current bounding box of the model (for position tracking)
+ */
+ const getBoundingBox = () => {
+ if (!scene) return null;
+ const box = new THREE.Box3().setFromObject(scene);
+ return {
+ center: box.getCenter(new THREE.Vector3()),
+ min: box.min.clone(),
+ max: box.max.clone()
+ };
+ };
+
+ /**
+ * Force update the mixer to apply current animation pose
+ * Call this after playAnimation to ensure bones reflect the new pose
+ */
+ const forceUpdate = (deltaTime = 0) => {
+ if (mixer) {
+ mixer.update(deltaTime);
+ }
+ };
+
+ /**
+ * Get the head bone for procedural rotation
+ * Searches for bones with 'head' or 'neck' in the name
+ */
+ const getHeadBone = () => {
+ if (!scene) return null;
+
+ let headBone = null;
+ scene.traverse((child) => {
+ if (child.isBone && (
+ child.name.toLowerCase().includes('head') ||
+ child.name.toLowerCase().includes('neck')
+ )) {
+ // Prefer 'head' over 'neck'
+ if (!headBone || child.name.toLowerCase().includes('head')) {
+ headBone = child;
+ }
+ }
+ });
+ return headBone;
+ };
+
+ /**
+ * Set head rotation target - applied every frame after mixer updates
+ * @param {number} xRotation - X rotation (left/right turn) in radians
+ * @param {boolean} enabled - Whether to apply head rotation
+ */
+ const setHeadRotation = (xRotation = 0, enabled = true) => {
+ headRotation.current = { x: xRotation, enabled };
+ // Cache the head bone
+ if (!headBoneRef.current) {
+ headBoneRef.current = getHeadBone();
+ }
+ console.log(`[FoxModel] Head rotation set: X=${xRotation.toFixed(2)}, enabled=${enabled}`);
+ };
+
+ /**
+ * Apply head rotation AFTER animation mixer updates
+ * This ensures our rotation overrides the animation
+ */
+ useFrame(() => {
+ if (headRotation.current.enabled && headBoneRef.current) {
+ headBoneRef.current.rotation.x = headRotation.current.x;
+ }
+ });
+
+ /**
+ * Get the scene for bone access
+ */
+ const getScene = () => scene;
+
+ // Expose methods to parent via ref
+ useImperativeHandle(ref, () => ({
+ playAnimation,
+ stopAllAnimations,
+ getGroup: () => group.current,
+ getAnimationDuration,
+ getRootBonePosition,
+ getBoundingBox,
+ forceUpdate,
+ getMixer: () => mixer,
+ getHeadBone,
+ setHeadRotation,
+ getScene,
+ ANIMATIONS,
+ }));
+
+ // Use the scene directly without cloning
+ // Center component will handle geometry centering
+ console.log('[FoxModel] Rendering at position:', position, 'scale:', scale);
+
+ return (
+ <>
+ {/*
+ * Key fix: Use group for GSAP/position control
+ * Center component auto-centers the model geometry
+ */}
+
+
+
+
+
+ >
+ );
+});
+
+FoxModel.displayName = 'FoxModel';
+
+// Preload the model
+useGLTF.preload(MODEL_PATH);
+
+export default FoxModel;
+export { ANIMATIONS };
diff --git a/src/components/FoxMascot/index.jsx b/src/components/FoxMascot/index.jsx
new file mode 100644
index 00000000..5d7bc261
--- /dev/null
+++ b/src/components/FoxMascot/index.jsx
@@ -0,0 +1,8 @@
+/**
+ * @fileoverview FoxMascot Component Export
+ * @module components/FoxMascot
+ */
+
+export { default } from './FoxMascot';
+export { default as FoxModel, ANIMATIONS } from './FoxModel';
+export { default as useAnimationSequence, PHASES } from './useAnimationSequence';
diff --git a/src/components/FoxMascot/useAnimationSequence.js b/src/components/FoxMascot/useAnimationSequence.js
new file mode 100644
index 00000000..8d74f7c9
--- /dev/null
+++ b/src/components/FoxMascot/useAnimationSequence.js
@@ -0,0 +1,248 @@
+/**
+ * @fileoverview useAnimationSequence Hook - GSAP timeline for fox mascot animation
+ * Uses mathematical camera frustum calculation for screen-to-world conversion
+ * @module components/FoxMascot/useAnimationSequence
+ */
+
+import { useRef, useCallback, useEffect } from 'react';
+import gsap from 'gsap';
+
+/**
+ * Animation phases configuration
+ */
+const PHASES = {
+ EMERGE: 'emerge',
+ WALK_LEFT: 'walkLeft',
+ WALK_RIGHT: 'walkRight',
+ TRANSFORM: 'transform',
+};
+
+/**
+ * Hook to manage the complete fox mascot animation sequence using GSAP
+ * @param {Object} foxRef - Ref to FoxModel component
+ * @param {Function} onComplete - Callback when animation completes
+ * @param {Function} onPhaseChange - Callback when animation phase changes
+ * @returns {Object} Animation control methods
+ */
+const useAnimationSequence = (foxRef, onComplete, onPhaseChange) => {
+ const timeline = useRef(null);
+ const isPlaying = useRef(false);
+ const currentPhase = useRef(null);
+
+ /**
+ * Set current phase and notify
+ */
+ const setPhase = useCallback((phase) => {
+ currentPhase.current = phase;
+ if (onPhaseChange) {
+ onPhaseChange(phase);
+ }
+ }, [onPhaseChange]);
+
+ /**
+ * Calculate visible area at z=0 using camera frustum math
+ * Formula: visibleHeight = 2 * tan(FOV/2) * distance
+ * Camera: FOV=50, position z=100
+ */
+ const getVisibleBounds = useCallback(() => {
+ const fov = 50; // degrees (must match FoxMascot.jsx)
+ const cameraZ = 100; // must match FoxMascot.jsx
+ const aspectRatio = window.innerWidth / window.innerHeight;
+
+ // Convert FOV to radians and calculate visible height at z=0
+ const vFOVRad = (fov * Math.PI) / 180;
+ const visibleHeight = 2 * Math.tan(vFOVRad / 2) * cameraZ;
+ const visibleWidth = visibleHeight * aspectRatio;
+
+ return {
+ halfWidth: visibleWidth / 2,
+ halfHeight: visibleHeight / 2
+ };
+ }, []);
+
+ /**
+ * Calculate positions based on viewport using camera frustum math
+ */
+ const getPositions = useCallback(() => {
+ const bounds = getVisibleBounds();
+
+ // Position at 90% towards each edge (very close to chatbot icon)
+ const rightEdge = bounds.halfWidth * 0.90;
+ const bottomEdge = -bounds.halfHeight * 0.85;
+ const leftEdge = -bounds.halfWidth * 0.75;
+
+ console.log('[Fox] Visible bounds:', bounds);
+ console.log('[Fox] Positions: right=', rightEdge, 'bottom=', bottomEdge);
+
+ return {
+ startX: rightEdge, // Right side (near chatbot)
+ startY: bottomEdge, // Bottom
+ centerX: 0, // Center
+ leftEdgeX: leftEdge, // Left side
+ groundY: bottomEdge // Ground level
+ };
+ }, [getVisibleBounds]);
+
+ /**
+ * Build and play the master timeline
+ */
+ const play = useCallback(() => {
+ if (!foxRef.current || isPlaying.current) return;
+
+ const foxModel = foxRef.current;
+ const group = foxModel.getGroup();
+ if (!group) return;
+
+ const { startX, startY, centerX, leftEdgeX, groundY } = getPositions();
+
+ // Scale adjusted for mathematical positioning (previous was too small)
+ // With FOV=50, z=100, the visible area is ~93 units, so we need larger scale
+ const targetScale = 0.001;
+
+ console.log('[Fox] Animation starting with:', { startX, startY, groundY, targetScale });
+
+ // Kill any existing timeline
+ if (timeline.current) {
+ timeline.current.kill();
+ }
+
+ isPlaying.current = true;
+
+ // Create master timeline
+ timeline.current = gsap.timeline({
+ onComplete: () => {
+ isPlaying.current = false;
+ if (onComplete) onComplete();
+ },
+ });
+
+ const tl = timeline.current;
+
+ // Phase 1: Emerge from bottom-right (0s - 1s)
+ tl.call(() => {
+ setPhase(PHASES.EMERGE);
+ foxModel.playAnimation('Fox_Jump', 0.2);
+ })
+ .set(group.position, { x: startX, y: startY, z: 0 }) // Bottom-right corner
+ .set(group.rotation, { y: -Math.PI / 2 }) // Face left (-90 degrees)
+ // Pop in with scale
+ .to(group.scale, {
+ x: targetScale, y: targetScale, z: targetScale,
+ duration: 0.4,
+ ease: 'back.out(1.7)'
+ })
+ // Jump up slightly then land
+ .to(group.position, {
+ y: groundY + 3,
+ duration: 0.3,
+ ease: 'power2.out'
+ })
+ .to(group.position, {
+ y: groundY,
+ duration: 0.2,
+ ease: 'bounce.out'
+ })
+
+ // Phase 2: Walk left across screen (1s - 4s)
+ .call(() => {
+ setPhase(PHASES.WALK_LEFT);
+ foxModel.playAnimation('Fox_Walk', 0.3);
+ })
+ .to(group.position, {
+ x: centerX,
+ duration: 2.5,
+ ease: 'none',
+ })
+ // Stop in center, do idle animation
+ .call(() => foxModel.playAnimation('Fox_Idle', 0.2))
+ .to({}, { duration: 0.8 }) // Pause for idle
+ .call(() => foxModel.playAnimation('Fox_Sit_Yes', 0.3))
+ .to({}, { duration: 0.6 })
+
+ // Phase 3: Walk right / return (5s - 8s)
+ .call(() => {
+ setPhase(PHASES.WALK_RIGHT);
+ foxModel.playAnimation('Fox_Walk', 0.3);
+ })
+ // Turn around to face right
+ .to(group.rotation, { y: Math.PI * 0.5, duration: 0.3, ease: 'power2.inOut' })
+ .to(group.position, {
+ x: startX,
+ duration: 2.5,
+ ease: 'none',
+ })
+
+ // Phase 4: Transform back to icon (8s - 9s)
+ .call(() => {
+ setPhase(PHASES.TRANSFORM);
+ foxModel.playAnimation('Fox_Idle', 0.2);
+ })
+ // Move to bottom right corner
+ .to(group.position, {
+ y: startY,
+ duration: 0.2,
+ ease: 'power2.in'
+ })
+ // Shrink back to nothing
+ .to(group.scale, {
+ x: 0, y: 0, z: 0,
+ duration: 0.4,
+ ease: 'power2.in'
+ });
+
+ }, [foxRef, onComplete, onPhaseChange, setPhase, getPositions]);
+
+ /**
+ * Skip to end of animation
+ */
+ const skip = useCallback(() => {
+ if (timeline.current) {
+ timeline.current.progress(1);
+ timeline.current.kill();
+ }
+ isPlaying.current = false;
+ if (onComplete) onComplete();
+ }, [onComplete]);
+
+ /**
+ * Pause animation
+ */
+ const pause = useCallback(() => {
+ if (timeline.current) {
+ timeline.current.pause();
+ }
+ }, []);
+
+ /**
+ * Resume animation
+ */
+ const resume = useCallback(() => {
+ if (timeline.current) {
+ timeline.current.resume();
+ }
+ }, []);
+
+ /**
+ * Cleanup on unmount
+ */
+ useEffect(() => {
+ return () => {
+ if (timeline.current) {
+ timeline.current.kill();
+ }
+ };
+ }, []);
+
+ return {
+ play,
+ skip,
+ pause,
+ resume,
+ isPlaying: () => isPlaying.current,
+ getCurrentPhase: () => currentPhase.current,
+ PHASES,
+ };
+};
+
+export default useAnimationSequence;
+export { PHASES };
diff --git a/src/data/Sponser.json b/src/data/Sponser.json
index 9de63e67..9bb27730 100644
--- a/src/data/Sponser.json
+++ b/src/data/Sponser.json
@@ -1,25 +1,22 @@
[
- {
- "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b1e566f93e724d817493_eef89a14894ab60c2de4591433117cb8_347615ad16304501c571bf1208b83615bc2eb3b7.jpg"
- },
- {
- "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b277c3dff77928f42d07_42506cd40d9f845726c5b38fa6135dab_1737828471512.jpeg"
- },
- {
- "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/6898aa1700b8583e9b0cea09_9a90e8c4f7933718410931fff16e1d67d0b1a728%20(1)-modified.png"
- },
- {
- "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b4a473e0c91daf83aee0_SR%20burger%20point%202.png"
- },
-
- {
- "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/6898a9a97a1a9e6121d28cfb_988e7f74979dc9924fdc07590a81526d0d19ddc4.png"
- },
-
- {
- "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b4713f09aa52852051a1_Adda%20Cafe%20-%20Logo%202.png"
- },
-
+ {
+ "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b1e566f93e724d817493_eef89a14894ab60c2de4591433117cb8_347615ad16304501c571bf1208b83615bc2eb3b7.jpg"
+ },
+ {
+ "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b277c3dff77928f42d07_42506cd40d9f845726c5b38fa6135dab_1737828471512.jpeg"
+ },
+ {
+ "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/6898aa1700b8583e9b0cea09_9a90e8c4f7933718410931fff16e1d67d0b1a728%20(1)-modified.png"
+ },
+ {
+ "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b4a473e0c91daf83aee0_SR%20burger%20point%202.png"
+ },
+ {
+ "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/6898a9a97a1a9e6121d28cfb_988e7f74979dc9924fdc07590a81526d0d19ddc4.png"
+ },
+ {
+ "image": "https://cdn.prod.website-files.com/6898a84f39288fa31fb19eb3/68b1b4713f09aa52852051a1_Adda%20Cafe%20-%20Logo%202.png"
+ },
{
"image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474142/WebImages/idm0kbdoybdmiklcgvkv.png"
},
@@ -59,17 +56,43 @@
{
"image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474142/WebImages/jvvudiuohzmgczo188us.png"
},
-
{
"image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723476284/WebImages/t4bje7l7nqvusixwr423.jpg"
},
{
"image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474152/WebImages/ox8zpeciavvell8jvmgd.png"
},
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618449/Confique_sgmn7g.png"
+ },
{
"image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474144/WebImages/tqkublfvrp45lut6pvva.png"
},
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618655/Anime_no_Sekai_mapjx2.png"
+ },
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618655/Teology_od0lpf.png"
+ },
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618656/Beardo_nzyawr.png"
+ },
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618656/Exsolvia_rmueuy.png"
+ },
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618656/Pure_Veggie_Bliss_jkhyzu.png"
+ },
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618656/Marg_tara_y1mied.png"
+ },
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618657/Kiti_Kitchen_oekgkf.png"
+ },
+ {
+ "image": "https://res.cloudinary.com/daq1wz35t/image/upload/v1771618658/Short_Cut_ngohps.png"
+ },
{
"image": "https://res.cloudinary.com/dm6jd6bhk/image/upload/v1723474145/WebImages/ctvvq74mtiltxhiniwsf.png"
}
-]
+]
\ No newline at end of file
diff --git a/src/layouts/Footer/Footer.jsx b/src/layouts/Footer/Footer.jsx
index 3f96e5b4..0ddea54d 100644
--- a/src/layouts/Footer/Footer.jsx
+++ b/src/layouts/Footer/Footer.jsx
@@ -34,25 +34,22 @@ export default function Footer() {
Explore
Home
Events
Team
@@ -64,17 +61,15 @@ export default function Footer() {
Contact
Alumni
@@ -82,9 +77,8 @@ export default function Footer() {
href="http://medium.com/@fedkiit"
target="_blank"
rel="noopener noreferrer"
- className={`${styles.footerleftlink} ${
- isOmega ? styles.omegaLink : ""
- }`}
+ className={`${styles.footerleftlink} ${isOmega ? styles.omegaLink : ""
+ }`}
>
Blog
@@ -95,18 +89,16 @@ export default function Footer() {
Manifesto
Partners
@@ -119,56 +111,48 @@ export default function Footer() {
href="https://www.linkedin.com/company/fedkiit/"
target="_blank"
rel="noopener noreferrer"
- className={`${styles.link1} ${
- isOmega ? styles.omegaSocialLink : ""
- }`}
+ className={`${styles.link1} ${isOmega ? styles.omegaSocialLink : ""
+ }`}
>
@@ -185,24 +169,22 @@ export default function Footer() {
Terms and conditions
&
Privacy policy
-
© 2024, fedkiit
+
© {new Date().getFullYear()}, fedkiit
{/* */}