diff --git a/components.json b/components.json new file mode 100644 index 0000000..1f970e6 --- /dev/null +++ b/components.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@react-bits": "https://reactbits.dev/r/{name}.json" + } +} diff --git a/package-lock.json b/package-lock.json index 0402af3..d14f7dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,20 +8,26 @@ "name": "profit-planet-frontend", "version": "0.1.0", "dependencies": { + "@gsap/react": "^2.1.2", "@headlessui/react": "^2.2.9", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^5.2.2", "@lottiefiles/react-lottie-player": "^3.6.0", "@react-pdf/renderer": "^4.3.0", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.19", "@tailwindplus/elements": "^1.0.15", "@tailwindui/react": "^0.1.1", "axios": "^1.12.2", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "country-flag-icons": "^1.5.21", "country-select-js": "^2.1.0", + "gsap": "^3.14.2", "intl-tel-input": "^25.15.0", + "lucide-react": "^0.562.0", "motion": "^12.23.22", "next": "^16.0.7", "pdfjs-dist": "^5.4.149", @@ -33,6 +39,9 @@ "react-pdf": "^10.1.0", "react-phone-number-input": "^3.4.12", "react-toastify": "^11.0.5", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7", + "three": "^0.167.1", "winston": "^3.17.0", "yup": "^1.7.1", "zustand": "^5.0.8" @@ -1634,6 +1643,12 @@ "kuler": "^2.0.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", @@ -1874,6 +1889,16 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@gsap/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", + "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", + "license": "SEE LICENSE AT https://gsap.com/standard-license", + "peerDependencies": { + "gsap": "^3.12.5", + "react": ">=17" + } + }, "node_modules/@headlessui/react": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", @@ -2465,6 +2490,24 @@ "react": "16 - 19" } }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@napi-rs/canvas": { "version": "0.1.80", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", @@ -3117,6 +3160,95 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", + "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", + "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@react-types/shared": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.0.tgz", @@ -3523,6 +3655,12 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3534,6 +3672,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3565,11 +3709,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.15.tgz", "integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==", - "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -3586,12 +3735,49 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.182.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz", + "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.44.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz", @@ -4150,6 +4336,30 @@ "win32" ] }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.68", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.68.tgz", + "integrity": "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==", + "license": "BSD-3-Clause" + }, "node_modules/abs-svg-path": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/abs-svg-path/-/abs-svg-path-0.1.1.tgz", @@ -4625,6 +4835,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4684,6 +4918,19 @@ "node": ">=6" } }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001745", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", @@ -4731,6 +4978,18 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -4873,11 +5132,28 @@ "integrity": "sha512-T7gM2MT6S06lGqqkkBCmWFlyryKuaBgbeJFFxZttT+GT6pwl63r5KuLQszkfbtL9YEu+8JvrRayfvyrZd9I++g==", "license": "MIT" }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5146,6 +5422,15 @@ "node": ">=6" } }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", @@ -5175,6 +5460,12 @@ "node": ">=0.10.0" } }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5926,6 +6217,12 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6277,6 +6574,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, "node_modules/goober": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", @@ -6312,6 +6615,13 @@ "dev": true, "license": "MIT" }, + "node_modules/gsap": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", + "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license.", + "peer": true + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6403,6 +6713,12 @@ "node": ">= 0.4" } }, + "node_modules/hls.js": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", + "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "license": "Apache-2.0" + }, "node_modules/hsl-to-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hsl-to-hex/-/hsl-to-hex-1.0.0.tgz", @@ -6424,6 +6740,26 @@ "integrity": "sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==", "license": "ISC" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6434,6 +6770,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -6771,6 +7113,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6945,7 +7293,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -6966,6 +7313,18 @@ "node": ">= 0.4" } }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, "node_modules/jay-peg": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jay-peg/-/jay-peg-1.1.1.tgz", @@ -7122,6 +7481,15 @@ "integrity": "sha512-RN3q3gImZ91BvRDYjWp7ICz3gRn81mW5L4SW+2afzNCC0I/nkXstBgZThQGTE3S/9q5J90FH4dP+TXx8NhdZKg==", "license": "MIT" }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", @@ -7453,6 +7821,25 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -7523,6 +7910,21 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -8051,7 +8453,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8859,6 +9260,12 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8869,6 +9276,16 @@ "node": ">= 0.8.0" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9080,6 +9497,21 @@ "react-dom": "^18 || ^19" } }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -9428,7 +9860,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -9441,7 +9872,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9557,6 +9987,32 @@ "node": "*" } }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -9765,6 +10221,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/svg-arc-to-cubic-bezier": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", @@ -9777,6 +10242,16 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "license": "MIT" }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", @@ -9784,6 +10259,15 @@ "license": "MIT", "peer": true }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", @@ -9821,6 +10305,45 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/three": { + "version": "0.167.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.167.1.tgz", + "integrity": "sha512-gYTLJA/UQip6J/tJvl91YYqlZF47+D/kxiWrbTon35ZHlXEN0VOo+Qke2walF1/x92v55H6enomymg4Dak52kw==", + "license": "MIT", + "peer": true + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, "node_modules/tiny-case": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", @@ -9916,6 +10439,36 @@ "node": ">= 14.0.0" } }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -9948,6 +10501,43 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10208,6 +10798,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/vite-compatible-readable-stream": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz", @@ -10231,11 +10830,21 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/package.json b/package.json index a876b2d..5106bc4 100644 --- a/package.json +++ b/package.json @@ -9,20 +9,26 @@ "lint": "eslint" }, "dependencies": { + "@gsap/react": "^2.1.2", "@headlessui/react": "^2.2.9", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^5.2.2", "@lottiefiles/react-lottie-player": "^3.6.0", "@react-pdf/renderer": "^4.3.0", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.5.0", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.19", "@tailwindplus/elements": "^1.0.15", "@tailwindui/react": "^0.1.1", "axios": "^1.12.2", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "country-flag-icons": "^1.5.21", "country-select-js": "^2.1.0", + "gsap": "^3.14.2", "intl-tel-input": "^25.15.0", + "lucide-react": "^0.562.0", "motion": "^12.23.22", "next": "^16.0.7", "pdfjs-dist": "^5.4.149", @@ -34,6 +40,9 @@ "react-pdf": "^10.1.0", "react-phone-number-input": "^3.4.12", "react-toastify": "^11.0.5", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7", + "three": "^0.167.1", "winston": "^3.17.0", "yup": "^1.7.1", "zustand": "^5.0.8" diff --git a/src/app/components/Beams.tsx b/src/app/components/Beams.tsx new file mode 100644 index 0000000..64b5aa6 --- /dev/null +++ b/src/app/components/Beams.tsx @@ -0,0 +1,372 @@ +import { forwardRef, useImperativeHandle, useEffect, useRef, useMemo, FC, ReactNode } from 'react'; + +import * as THREE from 'three'; + +import { Canvas, useFrame } from '@react-three/fiber'; +import { PerspectiveCamera } from '@react-three/drei'; +import { degToRad } from 'three/src/math/MathUtils.js'; + +type UniformValue = THREE.IUniform | unknown; + +interface ExtendMaterialConfig { + header: string; + vertexHeader?: string; + fragmentHeader?: string; + material?: THREE.MeshPhysicalMaterialParameters & { fog?: boolean }; + uniforms?: Record; + vertex?: Record; + fragment?: Record; +} + +type ShaderWithDefines = THREE.ShaderLibShader & { + defines?: Record; +}; + +function extendMaterial( + BaseMaterial: new (params?: THREE.MaterialParameters) => T, + cfg: ExtendMaterialConfig +): THREE.ShaderMaterial { + const physical = THREE.ShaderLib.physical as ShaderWithDefines; + const { vertexShader: baseVert, fragmentShader: baseFrag, uniforms: baseUniforms } = physical; + const baseDefines = physical.defines ?? {}; + + const uniforms: Record = THREE.UniformsUtils.clone(baseUniforms); + + const defaults = new BaseMaterial(cfg.material || {}) as T & { + color?: THREE.Color; + roughness?: number; + metalness?: number; + envMap?: THREE.Texture; + envMapIntensity?: number; + }; + + if (defaults.color) uniforms.diffuse.value = defaults.color; + if ('roughness' in defaults) uniforms.roughness.value = defaults.roughness; + if ('metalness' in defaults) uniforms.metalness.value = defaults.metalness; + if ('envMap' in defaults) uniforms.envMap.value = defaults.envMap; + if ('envMapIntensity' in defaults) uniforms.envMapIntensity.value = defaults.envMapIntensity; + + Object.entries(cfg.uniforms ?? {}).forEach(([key, u]) => { + uniforms[key] = + u !== null && typeof u === 'object' && 'value' in u + ? (u as THREE.IUniform) + : ({ value: u } as THREE.IUniform); + }); + + let vert = `${cfg.header}\n${cfg.vertexHeader ?? ''}\n${baseVert}`; + let frag = `${cfg.header}\n${cfg.fragmentHeader ?? ''}\n${baseFrag}`; + + for (const [inc, code] of Object.entries(cfg.vertex ?? {})) { + vert = vert.replace(inc, `${inc}\n${code}`); + } + for (const [inc, code] of Object.entries(cfg.fragment ?? {})) { + frag = frag.replace(inc, `${inc}\n${code}`); + } + + const mat = new THREE.ShaderMaterial({ + defines: { ...baseDefines }, + uniforms, + vertexShader: vert, + fragmentShader: frag, + lights: true, + fog: !!cfg.material?.fog + }); + + return mat; +} + +const CanvasWrapper: FC<{ children: ReactNode }> = ({ children }) => ( + + {children} + +); + +const hexToNormalizedRGB = (hex: string): [number, number, number] => { + const clean = hex.replace('#', ''); + const r = parseInt(clean.substring(0, 2), 16); + const g = parseInt(clean.substring(2, 4), 16); + const b = parseInt(clean.substring(4, 6), 16); + return [r / 255, g / 255, b / 255]; +}; + +const noise = ` +float random (in vec2 st) { + return fract(sin(dot(st.xy, + vec2(12.9898,78.233)))* + 43758.5453123); +} +float noise (in vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + float a = random(i); + float b = random(i + vec2(1.0, 0.0)); + float c = random(i + vec2(0.0, 1.0)); + float d = random(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + + (c - a)* u.y * (1.0 - u.x) + + (d - b) * u.x * u.y; +} +vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);} +vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;} +vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);} +float cnoise(vec3 P){ + vec3 Pi0 = floor(P); + vec3 Pi1 = Pi0 + vec3(1.0); + Pi0 = mod(Pi0, 289.0); + Pi1 = mod(Pi1, 289.0); + vec3 Pf0 = fract(P); + vec3 Pf1 = Pf0 - vec3(1.0); + vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); + vec4 iy = vec4(Pi0.yy, Pi1.yy); + vec4 iz0 = Pi0.zzzz; + vec4 iz1 = Pi1.zzzz; + vec4 ixy = permute(permute(ix) + iy); + vec4 ixy0 = permute(ixy + iz0); + vec4 ixy1 = permute(ixy + iz1); + vec4 gx0 = ixy0 / 7.0; + vec4 gy0 = fract(floor(gx0) / 7.0) - 0.5; + gx0 = fract(gx0); + vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); + vec4 sz0 = step(gz0, vec4(0.0)); + gx0 -= sz0 * (step(0.0, gx0) - 0.5); + gy0 -= sz0 * (step(0.0, gy0) - 0.5); + vec4 gx1 = ixy1 / 7.0; + vec4 gy1 = fract(floor(gx1) / 7.0) - 0.5; + gx1 = fract(gx1); + vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); + vec4 sz1 = step(gz1, vec4(0.0)); + gx1 -= sz1 * (step(0.0, gx1) - 0.5); + gy1 -= sz1 * (step(0.0, gy1) - 0.5); + vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); + vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); + vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); + vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); + vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); + vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); + vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); + vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); + vec4 norm0 = taylorInvSqrt(vec4(dot(g000,g000),dot(g010,g010),dot(g100,g100),dot(g110,g110))); + g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w; + vec4 norm1 = taylorInvSqrt(vec4(dot(g001,g001),dot(g011,g011),dot(g101,g101),dot(g111,g111))); + g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w; + float n000 = dot(g000, Pf0); + float n100 = dot(g100, vec3(Pf1.x,Pf0.yz)); + float n010 = dot(g010, vec3(Pf0.x,Pf1.y,Pf0.z)); + float n110 = dot(g110, vec3(Pf1.xy,Pf0.z)); + float n001 = dot(g001, vec3(Pf0.xy,Pf1.z)); + float n101 = dot(g101, vec3(Pf1.x,Pf0.y,Pf1.z)); + float n011 = dot(g011, vec3(Pf0.x,Pf1.yz)); + float n111 = dot(g111, Pf1); + vec3 fade_xyz = fade(Pf0); + vec4 n_z = mix(vec4(n000,n100,n010,n110),vec4(n001,n101,n011,n111),fade_xyz.z); + vec2 n_yz = mix(n_z.xy,n_z.zw,fade_xyz.y); + float n_xyz = mix(n_yz.x,n_yz.y,fade_xyz.x); + return 2.2 * n_xyz; +} +`; + +interface BeamsProps { + beamWidth?: number; + beamHeight?: number; + beamNumber?: number; + lightColor?: string; + speed?: number; + noiseIntensity?: number; + scale?: number; + rotation?: number; +} + +const Beams: FC = ({ + beamWidth = 2, + beamHeight = 15, + beamNumber = 12, + lightColor = '#ffffff', + speed = 2, + noiseIntensity = 1.75, + scale = 0.2, + rotation = 0 +}) => { + const meshRef = useRef>(null!); + + const beamMaterial = useMemo( + () => + extendMaterial(THREE.MeshStandardMaterial, { + header: ` + varying vec3 vEye; + varying float vNoise; + varying vec2 vUv; + varying vec3 vPosition; + uniform float time; + uniform float uSpeed; + uniform float uNoiseIntensity; + uniform float uScale; + ${noise}`, + vertexHeader: ` + float getPos(vec3 pos) { + vec3 noisePos = + vec3(pos.x * 0., pos.y - uv.y, pos.z + time * uSpeed * 3.) * uScale; + return cnoise(noisePos); + } + vec3 getCurrentPos(vec3 pos) { + vec3 newpos = pos; + newpos.z += getPos(pos); + return newpos; + } + vec3 getNormal(vec3 pos) { + vec3 curpos = getCurrentPos(pos); + vec3 nextposX = getCurrentPos(pos + vec3(0.01, 0.0, 0.0)); + vec3 nextposZ = getCurrentPos(pos + vec3(0.0, -0.01, 0.0)); + vec3 tangentX = normalize(nextposX - curpos); + vec3 tangentZ = normalize(nextposZ - curpos); + return normalize(cross(tangentZ, tangentX)); + }`, + fragmentHeader: '', + vertex: { + '#include ': `transformed.z += getPos(transformed.xyz);`, + '#include ': `objectNormal = getNormal(position.xyz);` + }, + fragment: { + '#include ': ` + float randomNoise = noise(gl_FragCoord.xy); + gl_FragColor.rgb -= randomNoise / 15. * uNoiseIntensity;` + }, + material: { fog: true }, + uniforms: { + diffuse: new THREE.Color(...hexToNormalizedRGB('#000000')), + time: { shared: true, mixed: true, linked: true, value: 0 }, + roughness: 0.3, + metalness: 0.3, + uSpeed: { shared: true, mixed: true, linked: true, value: speed }, + envMapIntensity: 10, + uNoiseIntensity: noiseIntensity, + uScale: scale + } + }), + [speed, noiseIntensity, scale] + ); + + return ( + + + + + + + + + + ); +}; + +function createStackedPlanesBufferGeometry( + n: number, + width: number, + height: number, + spacing: number, + heightSegments: number +): THREE.BufferGeometry { + const geometry = new THREE.BufferGeometry(); + const numVertices = n * (heightSegments + 1) * 2; + const numFaces = n * heightSegments * 2; + const positions = new Float32Array(numVertices * 3); + const indices = new Uint32Array(numFaces * 3); + const uvs = new Float32Array(numVertices * 2); + + let vertexOffset = 0; + let indexOffset = 0; + let uvOffset = 0; + const totalWidth = n * width + (n - 1) * spacing; + const xOffsetBase = -totalWidth / 2; + + for (let i = 0; i < n; i++) { + const xOffset = xOffsetBase + i * (width + spacing); + const uvXOffset = Math.random() * 300; + const uvYOffset = Math.random() * 300; + + for (let j = 0; j <= heightSegments; j++) { + const y = height * (j / heightSegments - 0.5); + const v0 = [xOffset, y, 0]; + const v1 = [xOffset + width, y, 0]; + positions.set([...v0, ...v1], vertexOffset * 3); + + const uvY = j / heightSegments; + uvs.set([uvXOffset, uvY + uvYOffset, uvXOffset + 1, uvY + uvYOffset], uvOffset); + + if (j < heightSegments) { + const a = vertexOffset, + b = vertexOffset + 1, + c = vertexOffset + 2, + d = vertexOffset + 3; + indices.set([a, b, c, c, b, d], indexOffset); + indexOffset += 6; + } + vertexOffset += 2; + uvOffset += 4; + } + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2)); + geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + geometry.computeVertexNormals(); + return geometry; +} + +const MergedPlanes = forwardRef< + THREE.Mesh, + { + material: THREE.ShaderMaterial; + width: number; + count: number; + height: number; + } +>(({ material, width, count, height }, ref) => { + const mesh = useRef>(null!); + useImperativeHandle(ref, () => mesh.current); + const geometry = useMemo( + () => createStackedPlanesBufferGeometry(count, width, height, 0, 100), + [count, width, height] + ); + useFrame((_, delta) => { + mesh.current.material.uniforms.time.value += 0.1 * delta; + }); + return ; +}); +MergedPlanes.displayName = 'MergedPlanes'; + +const PlaneNoise = forwardRef< + THREE.Mesh, + { + material: THREE.ShaderMaterial; + width: number; + count: number; + height: number; + } +>((props, ref) => ( + +)); +PlaneNoise.displayName = 'PlaneNoise'; + +const DirLight: FC<{ position: [number, number, number]; color: string }> = ({ position, color }) => { + const dir = useRef(null!); + useEffect(() => { + if (!dir.current) return; + const cam = dir.current.shadow.camera as THREE.Camera & { + top: number; + bottom: number; + left: number; + right: number; + far: number; + }; + cam.top = 24; + cam.bottom = -24; + cam.left = -24; + cam.right = 24; + cam.far = 64; + dir.current.shadow.bias = -0.004; + }, []); + return ; +}; + +export default Beams; diff --git a/src/app/components/Crosshair.tsx b/src/app/components/Crosshair.tsx new file mode 100644 index 0000000..869c63a --- /dev/null +++ b/src/app/components/Crosshair.tsx @@ -0,0 +1,198 @@ +'use client' + +import React, { useEffect, useRef, RefObject } from 'react' +import { gsap } from 'gsap' + +const lerp = (a: number, b: number, n: number): number => (1 - n) * a + n * b + +const getMousePos = (e: MouseEvent, container?: HTMLElement | null): { x: number; y: number } => { + if (container) { + const bounds = container.getBoundingClientRect() + return { + x: e.clientX - bounds.left, + y: e.clientY - bounds.top, + } + } + return { x: e.clientX, y: e.clientY } +} + +interface CrosshairProps { + color?: string + containerRef?: RefObject | null +} + +const Crosshair: React.FC = ({ color = 'white', containerRef = null }) => { + const cursorRef = useRef(null) + const lineHorizontalRef = useRef(null) + const lineVerticalRef = useRef(null) + const filterXRef = useRef(null) + const filterYRef = useRef(null) + + let mouse = { x: 0, y: 0 } + + useEffect(() => { + const handleMouseMove = (ev: Event) => { + const mouseEvent = ev as MouseEvent + mouse = getMousePos(mouseEvent, containerRef?.current || undefined) + + if (containerRef?.current) { + const bounds = containerRef.current.getBoundingClientRect() + if ( + mouseEvent.clientX < bounds.left || + mouseEvent.clientX > bounds.right || + mouseEvent.clientY < bounds.top || + mouseEvent.clientY > bounds.bottom + ) { + gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 }) + } else { + gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 1 }) + } + } + } + + const target: HTMLElement | Window = containerRef?.current || window + target.addEventListener('mousemove', handleMouseMove) + + const renderedStyles: { + [key: string]: { previous: number; current: number; amt: number } + } = { + tx: { previous: 0, current: 0, amt: 0.15 }, + ty: { previous: 0, current: 0, amt: 0.15 }, + } + + gsap.set([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { opacity: 0 }) + + const onMouseMove = (ev: Event) => { + const mouseEvent = ev as MouseEvent + mouse = getMousePos(mouseEvent, containerRef?.current || undefined) + + renderedStyles.tx.previous = renderedStyles.tx.current = mouse.x + renderedStyles.ty.previous = renderedStyles.ty.current = mouse.y + + gsap.to([lineHorizontalRef.current, lineVerticalRef.current].filter(Boolean), { + duration: 0.9, + ease: 'Power3.easeOut', + opacity: 1, + }) + + requestAnimationFrame(render) + + target.removeEventListener('mousemove', onMouseMove) + } + + target.addEventListener('mousemove', onMouseMove) + + const primitiveValues = { turbulence: 0 } + + const tl = gsap + .timeline({ + paused: true, + onStart: () => { + if (lineHorizontalRef.current) { + lineHorizontalRef.current.style.filter = 'url(#filter-noise-x)' + } + if (lineVerticalRef.current) { + lineVerticalRef.current.style.filter = 'url(#filter-noise-y)' + } + }, + onUpdate: () => { + if (filterXRef.current && filterYRef.current) { + filterXRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString()) + filterYRef.current.setAttribute('baseFrequency', primitiveValues.turbulence.toString()) + } + }, + onComplete: () => { + if (lineHorizontalRef.current) lineHorizontalRef.current.style.filter = 'none' + if (lineVerticalRef.current) lineVerticalRef.current.style.filter = 'none' + }, + }) + .to(primitiveValues, { + duration: 0.5, + ease: 'power1', + startAt: { turbulence: 1 }, + turbulence: 0, + }) + + const enter = () => tl.restart() + const leave = () => { + tl.progress(1).kill() + } + + const render = () => { + renderedStyles.tx.current = mouse.x + renderedStyles.ty.current = mouse.y + + for (const key in renderedStyles) { + const style = renderedStyles[key] + style.previous = lerp(style.previous, style.current, style.amt) + } + + if (lineHorizontalRef.current && lineVerticalRef.current) { + gsap.set(lineVerticalRef.current, { x: renderedStyles.tx.previous }) + gsap.set(lineHorizontalRef.current, { y: renderedStyles.ty.previous }) + } + + requestAnimationFrame(render) + } + + const links: NodeListOf = containerRef?.current + ? containerRef.current.querySelectorAll('a') + : document.querySelectorAll('a') + + links.forEach(link => { + link.addEventListener('mouseenter', enter) + link.addEventListener('mouseleave', leave) + }) + + return () => { + target.removeEventListener('mousemove', handleMouseMove) + target.removeEventListener('mousemove', onMouseMove) + links.forEach(link => { + link.removeEventListener('mouseenter', enter) + link.removeEventListener('mouseleave', leave) + }) + } + }, [containerRef]) + + return ( +
+ + + + + + + + + + + + +
+
+
+ ) +} + +export default Crosshair diff --git a/src/app/components/SplitText.tsx b/src/app/components/SplitText.tsx new file mode 100644 index 0000000..c449442 --- /dev/null +++ b/src/app/components/SplitText.tsx @@ -0,0 +1,223 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { SplitText as GSAPSplitText } from 'gsap/SplitText'; +import { useGSAP } from '@gsap/react'; + +gsap.registerPlugin(ScrollTrigger, GSAPSplitText, useGSAP); + +export interface SplitTextProps { + text: string; + className?: string; + delay?: number; + duration?: number; + ease?: string | ((t: number) => number); + splitType?: 'chars' | 'words' | 'lines' | 'words, chars'; + from?: gsap.TweenVars; + to?: gsap.TweenVars; + threshold?: number; + rootMargin?: string; + tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span'; + textAlign?: React.CSSProperties['textAlign']; + onLetterAnimationComplete?: () => void; +} + +const SplitText: React.FC = ({ + text, + className = '', + delay = 50, + duration = 1.25, + ease = 'power3.out', + splitType = 'chars', + from = { opacity: 0, y: 40 }, + to = { opacity: 1, y: 0 }, + threshold = 0.1, + rootMargin = '-100px', + tag = 'p', + textAlign = 'center', + onLetterAnimationComplete +}) => { + const ref = useRef(null); + const animationCompletedRef = useRef(false); + const onCompleteRef = useRef(onLetterAnimationComplete); + const [fontsLoaded, setFontsLoaded] = useState(false); + + useEffect(() => { + onCompleteRef.current = onLetterAnimationComplete; + }, [onLetterAnimationComplete]); + + // Reset animation completion when text changes so we can re-split/re-animate + useEffect(() => { + animationCompletedRef.current = false; + }, [text]); + + useEffect(() => { + if (document.fonts.status === 'loaded') { + setFontsLoaded(true); + } else { + document.fonts.ready.then(() => { + setFontsLoaded(true); + }); + } + }, []); + + useGSAP( + () => { + if (!ref.current || !text || !fontsLoaded) return; + if (animationCompletedRef.current) return; + + const el = ref.current as HTMLElement & { + _rbsplitInstance?: GSAPSplitText; + }; + + if (el._rbsplitInstance) { + try { + el._rbsplitInstance.revert(); + } catch (_) {} + el._rbsplitInstance = undefined; + } + + const startPct = (1 - threshold) * 100; + const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(rootMargin); + const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0; + const marginUnit = marginMatch ? marginMatch[2] || 'px' : 'px'; + const sign = + marginValue === 0 + ? '' + : marginValue < 0 + ? `-=${Math.abs(marginValue)}${marginUnit}` + : `+=${marginValue}${marginUnit}`; + const start = `top ${startPct}%${sign}`; + + let targets: Element[] = []; + const assignTargets = (self: GSAPSplitText) => { + if (splitType.includes('chars') && (self as GSAPSplitText).chars?.length) + targets = (self as GSAPSplitText).chars; + if (!targets.length && splitType.includes('words') && self.words.length) targets = self.words; + if (!targets.length && splitType.includes('lines') && self.lines.length) targets = self.lines; + if (!targets.length) targets = self.chars || self.words || self.lines; + }; + + const splitInstance = new GSAPSplitText(el, { + type: splitType, + smartWrap: true, + autoSplit: splitType === 'lines', + linesClass: 'split-line', + wordsClass: 'split-word', + charsClass: 'split-char', + reduceWhiteSpace: false, + onSplit: (self: GSAPSplitText) => { + assignTargets(self); + return gsap.fromTo( + targets, + { ...from }, + { + ...to, + duration, + ease, + stagger: delay / 1000, + scrollTrigger: { + trigger: el, + start, + once: true, + fastScrollEnd: true, + anticipatePin: 0.4 + }, + onComplete: () => { + animationCompletedRef.current = true; + onCompleteRef.current?.(); + }, + willChange: 'transform, opacity', + force3D: true + } + ); + } + }); + + el._rbsplitInstance = splitInstance; + + return () => { + ScrollTrigger.getAll().forEach(st => { + if (st.trigger === el) st.kill(); + }); + try { + splitInstance.revert(); + } catch (_) {} + el._rbsplitInstance = undefined; + }; + }, + { + dependencies: [ + text, + delay, + duration, + ease, + splitType, + JSON.stringify(from), + JSON.stringify(to), + threshold, + rootMargin, + fontsLoaded + ], + scope: ref + } + ); + + const renderTag = () => { + const style: React.CSSProperties = { + textAlign, + wordWrap: 'break-word', + willChange: 'transform, opacity' + }; + const classes = `split-parent overflow-hidden inline-block whitespace-normal ${className}`; + + switch (tag) { + case 'h1': + return ( +

+ {text} +

+ ); + case 'h2': + return ( +

+ {text} +

+ ); + case 'h3': + return ( +

+ {text} +

+ ); + case 'h4': + return ( +

+ {text} +

+ ); + case 'h5': + return ( +
+ {text} +
+ ); + case 'h6': + return ( +
+ {text} +
+ ); + default: + return ( +

+ {text} +

+ ); + } + }; + + return renderTag(); +}; + +export default SplitText; diff --git a/src/app/components/nav/Header.tsx b/src/app/components/nav/Header.tsx index 3bf3ca0..68ff456 100644 --- a/src/app/components/nav/Header.tsx +++ b/src/app/components/nav/Header.tsx @@ -1,7 +1,7 @@ 'use client' import { useState, useEffect, useCallback, useRef } from 'react' -import { useRouter } from 'next/navigation' +import { useRouter, usePathname } from 'next/navigation' import Image from 'next/image' import { Dialog, @@ -61,11 +61,13 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mounted, setMounted] = useState(false) const [animateIn, setAnimateIn] = useState(false) + const [scrollY, setScrollY] = useState(0) const user = useAuthStore(s => s.user) const logout = useAuthStore(s => s.logout) const accessToken = useAuthStore(s => s.accessToken) const refreshAuthToken = useAuthStore(s => s.refreshAuthToken) const router = useRouter() + const pathname = usePathname() const [hasReferralPerm, setHasReferralPerm] = useState(false) const [adminMgmtOpen, setAdminMgmtOpen] = useState(false) @@ -116,6 +118,38 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { setAnimateIn(true) }, []) + // Home-page scroll listener: reveal header after first scroll with slight parallax + useEffect(() => { + if (!mounted) return + + if (pathname !== '/') { + // non-home: header always visible + setScrollY(100) + return + } + + const handleScroll = () => { + const y = window.scrollY || window.pageYOffset || 0 + setScrollY(y) + } + + const handleWheel = (e: WheelEvent) => { + // virtual scroll so header can reveal even if page cannot scroll + setScrollY(prev => { + const next = prev + e.deltaY + return Math.max(0, Math.min(next, 200)) + }) + } + + window.addEventListener('scroll', handleScroll, { passive: true }) + window.addEventListener('wheel', handleWheel, { passive: true }) + + return () => { + window.removeEventListener('scroll', handleScroll) + window.removeEventListener('wheel', handleWheel) + } + }, [mounted, pathname]) + // Fetch user permissions and set hasReferralPerm useEffect(() => { let cancelled = false @@ -273,13 +307,15 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { ((user as any)?.roles?.includes?.('admin')) ) const isAdmin = mounted && rawIsAdmin + const headerVisible = pathname !== '/' ? animateIn : animateIn && scrollY > 24 + const parallaxOffset = pathname === '/' ? Math.max(-16, -scrollY * 0.15) : 0 return (