diff --git a/next.config.ts b/next.config.ts index e9ffa30..3ecef7a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,16 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "images.unsplash.com", + port: "", + pathname: "/**", + }, + ], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index a5746c1..e54cfd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,15 +8,24 @@ "name": "my-dosaplace-3d", "version": "0.1.0", "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.6.1", + "framer-motion": "^12.42.2", + "gsap": "^3.15.0", + "lenis": "^1.3.25", + "lucide-react": "^0.470.0", + "motion": "^12.42.2", "next": "16.2.10", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "three": "^0.185.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.185.1", "eslint": "^9", "eslint-config-next": "16.2.10", "tailwindcss": "^4", @@ -228,6 +237,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -276,6 +294,12 @@ "node": ">=6.9.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.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -1035,6 +1059,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "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/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -1246,6 +1288,94 @@ "node": ">=12.4.0" } }, + "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.6.1", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz", + "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==", + "license": "MIT", + "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/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1533,6 +1663,12 @@ "tailwindcss": "4.3.2" } }, + "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.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -1544,6 +1680,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.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1575,6 +1717,12 @@ "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.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", @@ -1595,6 +1743,41 @@ "@types/react": "^19.2.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.185.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.1.tgz", + "integrity": "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "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.64.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", @@ -2214,6 +2397,24 @@ "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/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -2507,6 +2708,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.43", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", @@ -2519,6 +2740,15 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.16", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", @@ -2577,6 +2807,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.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2637,6 +2891,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.30001805", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", @@ -2714,11 +2981,28 @@ "dev": true, "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", @@ -2858,6 +3142,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2881,6 +3174,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", @@ -3613,6 +3912,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "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", @@ -3693,6 +3998,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3870,6 +4202,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/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3890,6 +4228,12 @@ "dev": true, "license": "ISC" }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4001,6 +4345,32 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", + "license": "Apache-2.0" + }, + "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", @@ -4011,6 +4381,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", @@ -4339,6 +4715,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", @@ -4495,7 +4877,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": { @@ -4516,6 +4897,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/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -4649,6 +5042,37 @@ "node": ">=0.10" } }, + "node_modules/lenis": { + "version": "1.3.25", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.25.tgz", + "integrity": "sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==", + "license": "MIT", + "workspaces": [ + "packages/*", + "playground", + "playground/*" + ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4663,6 +5087,15 @@ "node": ">= 0.8.0" } }, + "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.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -4970,6 +5403,25 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.470.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.470.0.tgz", + "integrity": "sha512-tqYODeoB3qU5gxH33IbL7IcF05EYYzsZQJqdM9HGGHwmoJMPvVLPHO6Plu6HNAfntucZQ41taEw6aUpwOtaoXg==", + "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.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5000,6 +5452,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": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5037,6 +5504,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", + "integrity": "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.42.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5413,7 +5921,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" @@ -5484,6 +5991,12 @@ "node": "^10 || ^12 || >=14" } }, + "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", @@ -5494,6 +6007,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", @@ -5565,6 +6088,21 @@ "dev": true, "license": "MIT" }, + "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/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5609,6 +6147,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5870,7 +6417,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" @@ -5883,7 +6429,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" @@ -5981,6 +6526,32 @@ "dev": true, "license": "MIT" }, + "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", @@ -6181,6 +6752,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/tailwindcss": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", @@ -6202,6 +6782,44 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/three": { + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", + "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", + "license": "MIT" + }, + "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/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6263,6 +6881,36 @@ "node": ">=8.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.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6308,6 +6956,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", @@ -6542,11 +7227,39 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/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" @@ -6699,6 +7412,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 0027b11..c39f7cf 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,24 @@ "lint": "eslint" }, "dependencies": { + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.6.1", + "framer-motion": "^12.42.2", + "gsap": "^3.15.0", + "lenis": "^1.3.25", + "lucide-react": "^0.470.0", + "motion": "^12.42.2", "next": "16.2.10", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "three": "^0.185.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.185.1", "eslint": "^9", "eslint-config-next": "16.2.10", "tailwindcss": "^4", diff --git a/public/images/chef_cooking.png b/public/images/chef_cooking.png new file mode 100644 index 0000000..45422b4 Binary files /dev/null and b/public/images/chef_cooking.png differ diff --git a/public/images/chettinad_exterior.png b/public/images/chettinad_exterior.png new file mode 100644 index 0000000..d33f9fd Binary files /dev/null and b/public/images/chettinad_exterior.png differ diff --git a/public/images/chettinad_interior.png b/public/images/chettinad_interior.png new file mode 100644 index 0000000..d998db4 Binary files /dev/null and b/public/images/chettinad_interior.png differ diff --git a/public/images/masala_dosa_luxury.png b/public/images/masala_dosa_luxury.png new file mode 100644 index 0000000..076c9cf Binary files /dev/null and b/public/images/masala_dosa_luxury.png differ diff --git a/public/images/menu_gopuram_sketch.png b/public/images/menu_gopuram_sketch.png new file mode 100644 index 0000000..e5db0db Binary files /dev/null and b/public/images/menu_gopuram_sketch.png differ diff --git a/public/images/menu_palace_sketch.png b/public/images/menu_palace_sketch.png new file mode 100644 index 0000000..c87a4e0 Binary files /dev/null and b/public/images/menu_palace_sketch.png differ diff --git a/public/images/my_dosa_place_exterior.png b/public/images/my_dosa_place_exterior.png new file mode 100644 index 0000000..4c26e44 Binary files /dev/null and b/public/images/my_dosa_place_exterior.png differ diff --git a/public/images/my_dosa_place_exterior_royal.jpg b/public/images/my_dosa_place_exterior_royal.jpg new file mode 100644 index 0000000..6680816 Binary files /dev/null and b/public/images/my_dosa_place_exterior_royal.jpg differ diff --git a/public/images/my_dosa_place_exterior_traditional.jpg b/public/images/my_dosa_place_exterior_traditional.jpg new file mode 100644 index 0000000..6680816 Binary files /dev/null and b/public/images/my_dosa_place_exterior_traditional.jpg differ diff --git a/public/images/my_dosa_place_exterior_webgl.jpg b/public/images/my_dosa_place_exterior_webgl.jpg new file mode 100644 index 0000000..6c0fb42 Binary files /dev/null and b/public/images/my_dosa_place_exterior_webgl.jpg differ diff --git a/public/images/my_dosa_place_exterior_widescreen.png b/public/images/my_dosa_place_exterior_widescreen.png new file mode 100644 index 0000000..4c26e44 Binary files /dev/null and b/public/images/my_dosa_place_exterior_widescreen.png differ diff --git a/public/images/my_dosa_place_food.png b/public/images/my_dosa_place_food.png new file mode 100644 index 0000000..d3a036a Binary files /dev/null and b/public/images/my_dosa_place_food.png differ diff --git a/public/images/my_dosa_place_food_royal.png b/public/images/my_dosa_place_food_royal.png new file mode 100644 index 0000000..8304d05 Binary files /dev/null and b/public/images/my_dosa_place_food_royal.png differ diff --git a/public/images/my_dosa_place_food_traditional.png b/public/images/my_dosa_place_food_traditional.png new file mode 100644 index 0000000..8304d05 Binary files /dev/null and b/public/images/my_dosa_place_food_traditional.png differ diff --git a/public/images/my_dosa_place_food_webgl.jpg b/public/images/my_dosa_place_food_webgl.jpg new file mode 100644 index 0000000..18674d7 Binary files /dev/null and b/public/images/my_dosa_place_food_webgl.jpg differ diff --git a/public/images/my_dosa_place_interior.png b/public/images/my_dosa_place_interior.png new file mode 100644 index 0000000..457b085 Binary files /dev/null and b/public/images/my_dosa_place_interior.png differ diff --git a/public/images/my_dosa_place_interior_royal.jpg b/public/images/my_dosa_place_interior_royal.jpg new file mode 100644 index 0000000..c1a0d16 Binary files /dev/null and b/public/images/my_dosa_place_interior_royal.jpg differ diff --git a/public/images/my_dosa_place_interior_traditional.jpg b/public/images/my_dosa_place_interior_traditional.jpg new file mode 100644 index 0000000..c1a0d16 Binary files /dev/null and b/public/images/my_dosa_place_interior_traditional.jpg differ diff --git a/public/images/my_dosa_place_interior_webgl.jpg b/public/images/my_dosa_place_interior_webgl.jpg new file mode 100644 index 0000000..48039f5 Binary files /dev/null and b/public/images/my_dosa_place_interior_webgl.jpg differ diff --git a/public/images/royal_dining_experience.png b/public/images/royal_dining_experience.png new file mode 100644 index 0000000..7b8106a Binary files /dev/null and b/public/images/royal_dining_experience.png differ diff --git a/public/images/south_indian_thali_luxury.png b/public/images/south_indian_thali_luxury.png new file mode 100644 index 0000000..8fb9d80 Binary files /dev/null and b/public/images/south_indian_thali_luxury.png differ diff --git a/public/images/south_indian_welcome.png b/public/images/south_indian_welcome.png new file mode 100644 index 0000000..fb2b3aa Binary files /dev/null and b/public/images/south_indian_welcome.png differ diff --git a/public/images/test_storefront_jpg.jpg b/public/images/test_storefront_jpg.jpg new file mode 100644 index 0000000..91dc773 Binary files /dev/null and b/public/images/test_storefront_jpg.jpg differ diff --git a/public/images/test_storefront_png.png b/public/images/test_storefront_png.png new file mode 100644 index 0000000..05b1190 Binary files /dev/null and b/public/images/test_storefront_png.png differ diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..e9a79e8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,117 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; -} - @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --color-maroon: #188754; + --color-maroon-dark: #0c5030; + --color-maroon-light: #2ea670; + --color-gold: #d8c396; + --color-gold-light: #e4d7bc; + --color-gold-dark: #bc9e65; + --color-wood: #1e1412; + --color-stone: #3A3B3C; + --color-brass: #d8c396; + --color-orange: #d8c396; + --color-darkbg: #050c08; /* Dark green-cocoa tone background */ + --color-darkbg-light: #0d1e14; + + --font-serif: var(--font-cinzel); + --font-sans: var(--font-montserrat); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +:root { + --background: #FAF7F2; /* Premium warm ivory background */ + --foreground: #1c2d24; /* Dark forest green-black text */ } body { - background: var(--background); + background-color: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-sans), sans-serif; + overflow-x: hidden; +} + +/* Custom Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} +::-webkit-scrollbar-track { + background: var(--background); +} +::-webkit-scrollbar-thumb { + background: var(--color-maroon); + border-radius: 4px; + border: 2px solid var(--background); +} +::-webkit-scrollbar-thumb:hover { + background: var(--color-gold); +} + +/* Custom transitions and glow effects */ +.text-glow { + text-shadow: 0 0 10px rgba(24, 135, 84, 0.15), 0 0 20px rgba(24, 135, 84, 0.05); +} + +.gold-border { + border-image: linear-gradient(to right, #bc9e65, #d8c396, #e4d7bc, #d8c396, #bc9e65) 1; +} + +.brass-glow { + box-shadow: 0 0 15px rgba(216, 195, 150, 0.2), inset 0 0 15px rgba(216, 195, 150, 0.1); +} + +.diya-flame { + filter: drop-shadow(0 0 12px rgba(230, 126, 34, 0.8)) drop-shadow(0 0 25px rgba(216, 195, 150, 0.6)); + animation: flicker 2s infinite alternate; +} + +@keyframes flicker { + 0% { transform: scale(1) rotate(-1deg); opacity: 0.95; } + 20% { transform: scale(1.05) rotate(1deg); opacity: 1; } + 40% { transform: scale(0.95) rotate(-2deg); opacity: 0.9; } + 60% { transform: scale(1.1) rotate(2deg); opacity: 1; } + 80% { transform: scale(1) rotate(-1deg); opacity: 0.95; } + 100% { transform: scale(1.05) rotate(0deg); opacity: 1; } +} + +.custom-cursor { + width: 24px; + height: 24px; + border: 2.5px solid #188754; /* Green accent cursor */ + border-radius: 50%; + position: fixed; + transform: translate(-50%, -50%); + pointer-events: none; + z-index: 9999; +} + +.custom-cursor-dot { + width: 6px; + height: 6px; + background-color: #188754; + border-radius: 50%; + position: fixed; + transform: translate(-50%, -50%); + pointer-events: none; + z-index: 9999; +} + +/* Hide default cursor on desktop */ +@media (min-width: 1024px) { + body, a, button, select, input, textarea { + cursor: none; + } +} + +/* Glassmorphism - Premium light layout */ +.glass { + background: rgba(253, 251, 247, 0.85); + backdrop-filter: blur(16px); + border: 1px solid rgba(24, 135, 84, 0.18); +} + +/* Hide custom cursor on touchscreens */ +@media (pointer: coarse) { + .custom-cursor-container { + display: none !important; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..040e324 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,29 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Cinzel, Montserrat } from "next/font/google"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const cinzel = Cinzel({ + variable: "--font-cinzel", subsets: ["latin"], + weight: ["400", "500", "600", "700", "800", "900"], }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const montserrat = Montserrat({ + variable: "--font-montserrat", subsets: ["latin"], + weight: ["300", "400", "500", "600", "700", "800"], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "My Dosa Place - Pure Vegetarian South Indian Heritage Dining", + description: "Experience the authentic culinary legacy of South India at My Dosa Place in Waterloo, ON. A premium dining experience featuring ghee-roasted dosas, sambar, and home-ground chutneys.", + keywords: ["My Dosa Place", "Waterloo South Indian Restaurant", "Pure Vegetarian Indian", "Waterloo Dosa", "Traditional Indian Feast", "Waterloo Dining"], + authors: [{ name: "My Dosa Place" }], + openGraph: { + title: "My Dosa Place - Pure Vegetarian South Indian Heritage Dining", + description: "Experience the authentic culinary legacy of South India at My Dosa Place in Waterloo, ON.", + images: ["/images/chettinad_exterior.png"], + } }; export default function RootLayout({ @@ -25,9 +34,11 @@ export default function RootLayout({ return ( - {children} + + {children} + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..c9c54fe 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,74 @@ -import Image from "next/image"; +"use client"; + +import { useEffect, useState } from "react"; +import SmoothScroll from "@/components/SmoothScroll"; +import EntranceExperience from "@/components/EntranceExperience"; +import RestaurantStory from "@/components/RestaurantStory"; +import SignatureFoods from "@/components/SignatureFoods"; +import ChefShowcase from "@/components/ChefShowcase"; +import InteriorGallery from "@/components/InteriorGallery"; +import Testimonials from "@/components/Testimonials"; +import EventsAccordion from "@/components/EventsAccordion"; +import MenuPreview from "@/components/MenuPreview"; +import ReservationForm from "@/components/ReservationForm"; +import RouteMap from "@/components/RouteMap"; +import Footer from "@/components/Footer"; + +import CustomCursor from "@/components/CustomCursor"; +import SoundManager from "@/components/SoundManager"; export default function Home() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
+
+ {/* Isolated wrapper for client-only widgets to prevent hydration insertion conflicts */} +
+ {mounted && } + {mounted && } +
+ + {/* Lenis Scroll syncing with GSAP triggers */} + + + {/* Sections 1 & 2: Landing Exterior Zoom & Door opening into Welcome reception */} + + + {/* Section 3: Heritage Story timeline */} + + + {/* Section 4: Signature Foods featuring interactive 3D WebGL plates */} + + + {/* Section 5: Chef Sundaram showcase with canvas heat waves */} + + + {/* Section 6: GSAP Horizontal space gallery */} + + + {/* Section 7: 3D-tilting Testimonials diaries */} + + + {/* Section 8: Expanding Special Events Accordion */} + + + {/* Section 9: Luxury Menu Categories preview */} + + + {/* Section 10: Reservation Form with canvas sparks */} + + + {/* Section 11: Cartographic Route SVG Map with animated Chariot */} + + + {/* Section 12: Footer with hanging Toran, contact logs and floating diyas */} +
); } diff --git a/src/components/ChefShowcase.tsx b/src/components/ChefShowcase.tsx new file mode 100644 index 0000000..586afbd --- /dev/null +++ b/src/components/ChefShowcase.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import Image from "next/image"; +import { Sparkles, Flame, ShieldAlert } from "lucide-react"; + +export default function ChefShowcase() { + const canvasRef = useRef(null); + const containerRef = useRef(null); + const [activeSpice, setActiveSpice] = useState(0); + + // Spices database + const spices = [ + { + name: "Tellicherry Black Pepper", + role: "The Emperor of Heat", + desc: "Sourced from the high ranges of Malabar, these late-harvest vine berries provide a complex, fruity heat rather than a sharp sting, forming the backbone of our chettinad gravies.", + }, + { + name: "Green Cardamom", + role: "The Floral Note", + desc: "Harvested from the slopes of the Western Ghats. Cardamom pods are split by hand at dawn to capture the volatile essential oils that impart sweet floral notes to our Payasam.", + }, + { + name: "Star Anise", + role: "The Licorice Sweetness", + desc: "Imported since ancient times through maritime trade. It releases a warming, woody-licorice fragrance when roasted dry on iron pans for our heritage rice dishes.", + }, + ]; + + // Sparks and Embers Canvas Animation + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let animId: number; + let width = (canvas.width = canvas.parentElement?.clientWidth || 500); + let height = (canvas.height = canvas.parentElement?.clientHeight || 600); + + const handleResize = () => { + if (!canvas.parentElement) return; + width = canvas.width = canvas.parentElement.clientWidth; + height = canvas.height = canvas.parentElement.clientHeight; + }; + window.addEventListener("resize", handleResize); + + class Ember { + x: number; + y: number; + vx: number; + vy: number; + size: number; + life: number; + maxLife: number; + color: string; + + constructor() { + // start near the bottom-center of the image (simulating cooking pan) + this.x = width * 0.5 + (Math.random() - 0.5) * 150; + this.y = height * 0.75 + (Math.random() - 0.5) * 50; + this.vx = (Math.random() - 0.5) * 1.5; + this.vy = -Math.random() * 2.5 - 1.0; + this.size = Math.random() * 2.5 + 1.0; + this.maxLife = Math.random() * 80 + 40; + this.life = 0; + this.color = Math.random() > 0.4 ? "243, 229, 171" : "230, 126, 34"; // Gold or Orange sparks + } + + update() { + this.x += this.vx; + this.y += this.vy; + this.life++; + // Add draft wind effect + this.vx += (Math.random() - 0.5) * 0.1; + } + + draw(c: CanvasRenderingContext2D) { + const opacity = 1 - this.life / this.maxLife; + c.save(); + c.beginPath(); + c.fillStyle = `rgba(${this.color}, ${opacity})`; + c.shadowBlur = 8; + c.shadowColor = `rgb(${this.color})`; + c.arc(this.x, this.y, this.size, 0, Math.PI * 2); + c.fill(); + c.restore(); + } + } + + const embers: Ember[] = []; + const maxEmbers = 40; + + const render = () => { + ctx.clearRect(0, 0, width, height); + + // Spawn new embers + if (embers.length < maxEmbers && Math.random() > 0.4) { + embers.push(new Ember()); + } + + // Update and draw embers + for (let i = embers.length - 1; i >= 0; i--) { + const e = embers[i]; + e.update(); + e.draw(ctx); + + if (e.life >= e.maxLife || e.y < 0) { + embers.splice(i, 1); + } + } + + animId = requestAnimationFrame(render); + }; + + render(); + + return () => { + window.removeEventListener("resize", handleResize); + cancelAnimationFrame(animId); + }; + }, []); + + return ( +
+
+ + {/* Left Side: Chef Info & Spice Selection */} +
+ + T H E M A S T E R A L C H E M I S T + + +

+ CHEF SUNDARAM
+ + IYER + +

+ +

+ With three decades of culinary devotion, Chef Sundaram has traversed the villages of Tamil Nadu, + uncovering regional temple recipes and masterfully blending heritage masalas. He believes that true + gastronomy is a spiritual act, uniting soil, tradition, and warm wood fire. +

+ + {/* Interactive Spice Guide Wrapper */} +
+
+

+ Heritage Spice Altar +

+ + {/* Spice Tabs Selector */} +
+ {spices.map((spice, idx) => ( + + ))} +
+ + {/* Content pane with slide/fade animation */} +
+ + + + {spices[activeSpice].role} + +

+ {spices[activeSpice].name} +

+

+ {spices[activeSpice].desc} +

+
+
+
+
+
+ + {/* Right Side: Chef Cooking Portrait with embers canvas */} +
+ + {/* Framed Image Container */} +
+ {/* Brass Inner Frame Border */} +
+ + {/* Action Portrait */} +
+ Chef Sundaram Cooking with fire +
+ + {/* Sparks/Embers Canvas Overlay */} + + + {/* Subtle heat wave distortion visual overlay */} +
+
+ + {/* Decorative Corner Filigrees outside */} +
+
+
+ +
+
+ ); +} diff --git a/src/components/CustomCursor.tsx b/src/components/CustomCursor.tsx new file mode 100644 index 0000000..0ad9bd4 --- /dev/null +++ b/src/components/CustomCursor.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { motion, useMotionValue, useSpring } from "framer-motion"; + +export default function CustomCursor() { + const [isHovered, setIsHovered] = useState(false); + + // Motion values for smooth cursor tracking + const cursorX = useMotionValue(-100); + const cursorY = useMotionValue(-100); + + // Spring physics for trailing effect on the outer circle + const springConfig = { damping: 30, stiffness: 220, mass: 0.6 }; + const cursorXSpring = useSpring(cursorX, springConfig); + const cursorYSpring = useSpring(cursorY, springConfig); + + useEffect(() => { + const moveCursor = (e: MouseEvent) => { + cursorX.set(e.clientX); + cursorY.set(e.clientY); + }; + + const handleMouseOver = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if ( + target.tagName === "A" || + target.tagName === "BUTTON" || + target.closest("a") || + target.closest("button") || + target.classList.contains("clickable") || + target.closest(".clickable") + ) { + setIsHovered(true); + } else { + setIsHovered(false); + } + }; + + window.addEventListener("mousemove", moveCursor); + window.addEventListener("mouseover", handleMouseOver); + + return () => { + window.removeEventListener("mousemove", moveCursor); + window.removeEventListener("mouseover", handleMouseOver); + }; + }, [cursorX, cursorY]); + + return ( +
+ {/* Outer Spring Ring */} + + {/* Inner Pinpoint Dot */} + +
+ ); +} diff --git a/src/components/EntranceExperience.tsx b/src/components/EntranceExperience.tsx new file mode 100644 index 0000000..a9cf56f --- /dev/null +++ b/src/components/EntranceExperience.tsx @@ -0,0 +1,418 @@ +"use client"; + +import { useEffect, useRef, useState, Suspense, useMemo } from "react"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import { motion, AnimatePresence } from "framer-motion"; +import { Canvas, useFrame, useThree } from "@react-three/fiber"; +import { useTexture } from "@react-three/drei"; +import * as THREE from "three"; + +gsap.registerPlugin(ScrollTrigger); + +// Custom shader source for storefront door clipping +const vertexShader = ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; + +const facadeFragmentShader = ` + uniform sampler2D uTexture; + uniform float uOpacity; + varying vec2 vUv; + void main() { + vec4 col = texture2D(uTexture, vUv); + // Discard double doors in center of storefront + if (vUv.x >= 0.468 && vUv.x <= 0.532 && vUv.y >= 0.15 && vUv.y <= 0.50) { + discard; + } + gl_FragColor = vec4(col.rgb, col.a * uOpacity); + } +`; + +const leftDoorFragmentShader = ` + uniform sampler2D uTexture; + uniform float uOpacity; + varying vec2 vUv; + void main() { + // Crop texture to left door leaf boundaries + vec2 uv = vec2(0.468 + vUv.x * 0.032, 0.15 + vUv.y * 0.35); + vec4 col = texture2D(uTexture, uv); + gl_FragColor = vec4(col.rgb, col.a * uOpacity); + } +`; + +const rightDoorFragmentShader = ` + uniform sampler2D uTexture; + uniform float uOpacity; + varying vec2 vUv; + void main() { + // Crop texture to right door leaf boundaries + vec2 uv = vec2(0.50 + vUv.x * 0.032, 0.15 + vUv.y * 0.35); + vec4 col = texture2D(uTexture, uv); + gl_FragColor = vec4(col.rgb, col.a * uOpacity); + } +`; + +// Helper notifier to hide CSS loader once textures resolve +function LoaderNotifier({ onLoad }: { onLoad: () => void }) { + useEffect(() => { + onLoad(); + }, [onLoad]); + return null; +} + +// 3D Cinematic flight renderer +function CinematicFlight({ progress }: { progress: number }) { + const exteriorTex = useTexture("/images/my_dosa_place_exterior_webgl.jpg"); + const interiorTex = useTexture("/images/my_dosa_place_interior_webgl.jpg"); + const foodTex = useTexture("/images/my_dosa_place_food_webgl.jpg"); + + const leftDoorRef = useRef(null); + const rightDoorRef = useRef(null); + + // Set texture filtering to high quality linear + useEffect(() => { + [exteriorTex, interiorTex, foodTex].forEach((tex) => { + tex.minFilter = THREE.LinearFilter; + tex.magFilter = THREE.LinearFilter; + tex.generateMipmaps = false; + }); + }, [exteriorTex, interiorTex, foodTex]); + + const { width, height } = useThree((state) => state.viewport); + + // Compute facade dimensions based on dusk photo aspect ratio + const aspect = 1920 / 864; + const planeH = height; + const planeW = planeH * aspect; + + // Door dimensions matching facade coordinate percentages + const dw = 0.032 * planeW; + const dh = 0.35 * planeH; + const doorY = -0.175 * planeH; + const leftHingeX = -0.032 * planeW; + const rightHingeX = 0.032 * planeW; + + const extGroupRef = useRef(null); + const intMeshRef = useRef(null); + const foodMeshRef = useRef(null); + + // Refs for materials to update opacities in the high-performance frame loop + const facadeMatRef = useRef(null); + const leftDoorMatRef = useRef(null); + const rightDoorMatRef = useRef(null); + const intMatRef = useRef(null); + const foodMatRef = useRef(null); + + // Smooth frame loop camera zoom and door swing animations + useFrame(() => { + // 1. Storefront zoom and centering translation + // Scale goes from 1.0 to 5.5 + const targetExtScale = 1.0 + progress * 4.5; + if (extGroupRef.current) { + extGroupRef.current.scale.setScalar(THREE.MathUtils.lerp(extGroupRef.current.scale.x, targetExtScale, 0.08)); + // Pan Y upward to center the doors as we scale + const currentScale = extGroupRef.current.scale.x; + const targetY = -doorY * (currentScale - 1.0); + extGroupRef.current.position.y = THREE.MathUtils.lerp(extGroupRef.current.position.y, targetY, 0.08); + } + + // 2. Door opening rotation (swing inward Y-axis) + let doorProgress = 0; + if (progress >= 0.08 && progress <= 0.25) { + doorProgress = (progress - 0.08) / 0.17; + } else if (progress > 0.25) { + doorProgress = 1; + } + const angle = (Math.PI / 1.8) * doorProgress; + + if (leftDoorRef.current) { + leftDoorRef.current.rotation.y = THREE.MathUtils.lerp(leftDoorRef.current.rotation.y, angle, 0.08); + } + if (rightDoorRef.current) { + rightDoorRef.current.rotation.y = THREE.MathUtils.lerp(rightDoorRef.current.rotation.y, -angle, 0.08); + } + + // 3. Interior Dining Hall zoom + // Scale goes from 1.0 to 2.6 + let intProgress = 0; + if (progress > 0.08) { + intProgress = Math.min(1, (progress - 0.08) / 0.62); + } + const targetIntScale = 1.0 + intProgress * 1.6; + if (intMeshRef.current) { + intMeshRef.current.scale.setScalar(THREE.MathUtils.lerp(intMeshRef.current.scale.x, targetIntScale, 0.08)); + } + + // 4. Food Platter landing zoom + // Scale goes from 0.8 to 1.15 + let foodProgress = 0; + if (progress > 0.58) { + foodProgress = Math.min(1, (progress - 0.58) / 0.27); + } + const targetFoodScale = 0.8 + foodProgress * 0.35; + if (foodMeshRef.current) { + foodMeshRef.current.scale.setScalar(THREE.MathUtils.lerp(foodMeshRef.current.scale.x, targetFoodScale, 0.08)); + } + + // 5. Reactive Opacity Updates inside useFrame to prevent unmounting stutters + const extOpacity = progress < 0.25 ? 1 : Math.max(0, 1 - (progress - 0.25) / 0.15); + + let intOpacity = 0; + if (progress >= 0.08 && progress <= 0.25) { + intOpacity = (progress - 0.08) / 0.17; + } else if (progress > 0.25 && progress < 0.60) { + intOpacity = 1; + } else if (progress >= 0.60 && progress <= 0.78) { + intOpacity = Math.max(0, 1 - (progress - 0.60) / 0.18); + } else if (progress > 0.78) { + intOpacity = 0; + } + + const foodOpacity = progress < 0.58 ? 0 : Math.min(1, (progress - 0.58) / 0.20); + + // Apply opacities to materials + if (facadeMatRef.current) facadeMatRef.current.uniforms.uOpacity.value = extOpacity; + if (leftDoorMatRef.current) leftDoorMatRef.current.uniforms.uOpacity.value = extOpacity; + if (rightDoorMatRef.current) rightDoorMatRef.current.uniforms.uOpacity.value = extOpacity; + if (intMatRef.current) intMatRef.current.opacity = intOpacity; + if (foodMatRef.current) foodMatRef.current.opacity = foodOpacity; + }); + + // Memoize initial shader uniforms + const facadeUniforms = useMemo(() => ({ + uTexture: { value: exteriorTex }, + uOpacity: { value: 1 } + }), [exteriorTex]); + + const leftDoorUniforms = useMemo(() => ({ + uTexture: { value: exteriorTex }, + uOpacity: { value: 1 } + }), [exteriorTex]); + + const rightDoorUniforms = useMemo(() => ({ + uTexture: { value: exteriorTex }, + uOpacity: { value: 1 } + }), [exteriorTex]); + + return ( + <> + + + + {/* Plane 1: Storefront Facade & 3D Swinging Door Panels (Z = 0.02) */} + + + + + + + {/* Left Door Hinge Pivot Group */} + + + + + + + + {/* Right Door Hinge Pivot Group */} + + + + + + + + + {/* Plane 2: Pillars Dining Hall (Z = 0.01) */} + + + + + + {/* Plane 3: South Indian Platter Table (Z = 0.0) */} + + + + + + ); +} + +export default function EntranceExperience() { + const containerRef = useRef(null); + const scrollIndicatorRef = useRef(null); + + const [scrollProgress, setScrollProgress] = useState(0); + const [loaded, setLoaded] = useState(false); + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + // Sync GSAP pinning scrub triggers + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const ctx = gsap.context(() => { + const scrollIndicator = scrollIndicatorRef.current; + + const tl = gsap.timeline({ + scrollTrigger: { + trigger: container, + start: "top top", + end: "+=450%", + scrub: 1.2, + pin: true, + onUpdate: (self) => { + setScrollProgress(self.progress); + }, + }, + }); + + // Initial fade-out of the scroll indicator + if (scrollIndicator) { + tl.to(scrollIndicator, { + opacity: 0, + y: -25, + duration: 1, + }); + } + }, container); + + return () => ctx.revert(); + }, []); + + return ( +
+ {/* Traditional loading spinner overlay */} + {!loaded && ( +
+
+ + Entering My Dosa Place... + +
+ )} + + {/* Three.js R3F WebGL Scene */} + {isMounted && ( +
+ + + + setLoaded(true)} /> + + +
+ )} + + {/* Screen edge lighting overlays */} +
+ + {/* ----------------- OVERLAY HERO ELEMENT (OUTSIDE STATE) ----------------- */} +
+ {/* Scroll Indicator */} +
+
+
+
+
+
+ + {/* ----------------- FOOD PLATTER STORYTELLING CARD (REVEALS AT END) ----------------- */} + + {scrollProgress > 0.75 && ( + + {/* Inner traditional gold frame matching the arch dome */} +
+ + + P U R E V E G E T A R I A N F E A S T + + +

+ A Royal Banquet +

+ +
+ +

+ Your table is set. Experience a royal banquet served on fresh banana leaves, where crispy ghee-roasted dosas, steaming lentil sambar, and hand-ground chutneys await you. +

+ + )} + + + {/* Styled inline keyframes animations */} + +
+ ); +} diff --git a/src/components/EventsAccordion.tsx b/src/components/EventsAccordion.tsx new file mode 100644 index 0000000..e6b9ea2 --- /dev/null +++ b/src/components/EventsAccordion.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState } from "react"; +import { motion } from "framer-motion"; +import { Calendar, Sparkles, Gift, Users, Award, ShieldAlert } from "lucide-react"; + +export default function EventsAccordion() { + const [hoveredIdx, setHoveredIdx] = useState(null); + + const events = [ + { + title: "Kalyana Feasts", + tamilTitle: "கல்யாண விருந்து", + subtitle: "WEDDING BANQUETS", + desc: "A grand imperial feast served to celebrate auspicious marital unions. Traditional multi-course menus served on fresh organic banana leaves with royal silver chalices, accommodating up to five hundred guests in heritage luxury.", + icon: Sparkles, + bgGradient: "from-[#fdfcf9] via-[#e4d7bc]/40 to-[#FAF7F2]", + accentColor: "#188754", + }, + { + title: "Ayush Dinners", + tamilTitle: "ஆயுள் விருந்து", + subtitle: "ANNIVERSARIES & BIRTHDAYS", + desc: "Honor milestones and life chapters. Our ancestral dining rooms provide an intimate, warm-lit atmosphere with traditional music ensembles and customized family menus curated by Chef Sundaram.", + icon: Gift, + bgGradient: "from-[#fdfcf9] via-[#e4d7bc]/20 to-[#FAF7F2]", + accentColor: "#bc9e65", + }, + { + title: "Sabha Assemblies", + tamilTitle: "சபை கூட்டங்கள்", + subtitle: "CORPORATE COLLABORATIONS", + desc: "Host partners and executives in private boardrooms crafted from vintage rosewood. Pair high-level meetings with gourmet curated South Indian starters, specialized filter coffee breaks, and impeccable service.", + icon: Award, + bgGradient: "from-[#fdfcf9] via-[#188754]/8 to-[#FAF7F2]", + accentColor: "#188754", + }, + { + title: "Kudumba Dining", + tamilTitle: "குடும்ப விருந்து", + subtitle: "GRAND FAMILY REUNIONS", + desc: "Bring generations together under one carved timber ceiling. We recreate the authentic community dining experience of South Indian heritage mansions, complete with traditional brass servers and folklore music.", + icon: Users, + bgGradient: "from-[#fdfcf9] via-[#bc9e65]/15 to-[#FAF7F2]", + accentColor: "#bc9e65", + }, + ]; + + return ( +
+ + {/* Background Decorative */} +
+ +
+ + {/* Title */} +
+ + C E L E B R A T I O N S + +

+ ROYAL ASSEMBLIES & EVENTS +

+
+
+ + {/* Horizontal Accordion Track */} +
+ {events.map((event, idx) => { + const isHovered = hoveredIdx === idx; + const isAnyHovered = hoveredIdx !== null; + const Icon = event.icon; + + // Compute dynamic flex basis + // Default: equal sizes. On hover: active scales up, rest shrink + let flexClass = "lg:flex-[1]"; + if (isAnyHovered) { + flexClass = isHovered ? "lg:flex-[3.5] bg-opacity-100" : "lg:flex-[0.8] opacity-60"; + } + + return ( +
setHoveredIdx(idx)} + onMouseLeave={() => setHoveredIdx(null)} + className={`clickable flex-1 flex flex-col justify-between p-6 md:p-8 rounded-xl border border-gold/10 transition-all duration-700 ease-out relative overflow-hidden group ${flexClass} bg-gradient-to-b ${event.bgGradient}`} + > + {/* Vintage Border overlay on hover */} +
+
+ + {/* Left Line Accent (Gold) */} +
+ + {/* Top Section: Icon & Header */} +
+
+
+ +
+ {/* Collapsed view Title details */} +
+ + {event.subtitle} + +

+ {event.title} +

+
+
+ + {/* Desktop Collapsed view rotated Title */} +
+

+ {event.title} +

+
+
+ + {/* Expanded content view (Desktop only hides when collapsed, shows when hovered) */} +
+ {/* Category meta */} +
+ + {event.subtitle} + +

+ {event.title} +

+

+ {event.tamilTitle} +

+
+ + {/* Description */} +

+ {event.desc} +

+ + {/* Inquire CTA Button */} + +
+ + {/* Bottom Number Indicator */} +
+ + 0{idx + 1} + +
+ +
+ ); + })} +
+ +
+
+ ); +} diff --git a/src/components/FoodCanvas.tsx b/src/components/FoodCanvas.tsx new file mode 100644 index 0000000..ce94a81 --- /dev/null +++ b/src/components/FoodCanvas.tsx @@ -0,0 +1,317 @@ +"use client"; + +import { useRef, useEffect, useState } from "react"; +import { Canvas, useFrame, useLoader } from "@react-three/fiber"; +import { Center, OrbitControls, useTexture } from "@react-three/drei"; +import * as THREE from "three"; + +// Individual Food Plate 3D Component +function FoodPlate({ activeIdx, isHovered }: { activeIdx: number; isHovered: boolean }) { + const plateRef = useRef(null); + + // Load food textures dynamically + const textures = [ + useTexture("/images/masala_dosa_luxury.png"), + useTexture("/images/south_indian_thali_luxury.png"), + ]; + + // Store target rotation/position for smooth spring interpolation + const targetRotationY = useRef(0); + const targetPositionY = useRef(0); + const targetScale = useRef(1); + + useFrame((state) => { + if (!plateRef.current) return; + + // 1. Continuous slow rotation + targetRotationY.current += 0.005; + + // 2. Mouse interactive tilt effect + const mouseX = state.pointer.x * 0.4; + const mouseY = state.pointer.y * 0.4; + + plateRef.current.rotation.x = THREE.MathUtils.lerp(plateRef.current.rotation.x, 0.65 - mouseY, 0.1); + plateRef.current.rotation.y = THREE.MathUtils.lerp(plateRef.current.rotation.y, targetRotationY.current + mouseX, 0.1); + + // 3. Hover lift effect & slow float oscillation + const hoverOffset = isHovered ? 0.3 : 0; + const floatOffset = Math.sin(state.clock.getElapsedTime() * 1.5) * 0.08; + targetPositionY.current = hoverOffset + floatOffset; + plateRef.current.position.y = THREE.MathUtils.lerp(plateRef.current.position.y, targetPositionY.current, 0.08); + + // Scale spring transition + targetScale.current = isHovered ? 1.08 : 1.0; + plateRef.current.scale.setScalar( + THREE.MathUtils.lerp(plateRef.current.scale.x, targetScale.current, 0.1) + ); + }); + + return ( + + {/* 1. Base Brass Plate */} + + + + + + {/* Brass Rim/lip details */} + + + + + + {/* 2. Banana Leaf Liner */} + + + + + + {/* 3. Photorealistic Food Mesh with texture projection */} + + + + + + ); +} + +// Spices / Ingredients flying around the plate +function FloatingIngredients({ activeIdx }: { activeIdx: number }) { + const count = 18; + const meshRef = useRef(null); + const particles = useRef>([]); + + useEffect(() => { + // Generate random ingredients positions and velocities + const temp: typeof particles.current = []; + for (let i = 0; i < count; i++) { + const isCurryLeaf = i % 3 === 0; + const isRedChili = i % 3 === 1; + + temp.push({ + pos: new THREE.Vector3( + (Math.random() - 0.5) * 8, + (Math.random() - 0.5) * 6 + 2, + (Math.random() - 0.5) * 4 + ), + speed: new THREE.Vector3( + (Math.random() - 0.5) * 0.02, + -Math.random() * 0.015 - 0.005, + (Math.random() - 0.5) * 0.01 + ), + rot: new THREE.Vector3(Math.random() * Math.PI, Math.random() * Math.PI, 0), + rotSpeed: new THREE.Vector3(Math.random() * 0.02, Math.random() * 0.02, 0), + scale: isCurryLeaf ? 0.25 : isRedChili ? 0.12 : 0.05, + color: isCurryLeaf ? "#2E7D32" : isRedChili ? "#D84315" : "#212121", // Green, Red, Black (mustard seeds) + }); + } + particles.current = temp; + }, [activeIdx]); + + useFrame(() => { + if (!meshRef.current) return; + + // Animate flying items + const children = meshRef.current.children; + particles.current.forEach((p, idx) => { + const child = children[idx] as THREE.Mesh; + if (!child) return; + + // Update position + p.pos.add(p.speed); + p.rot.add(p.rotSpeed); + + // Magnet effect: pull slightly towards center plate [0, 0, 0] + const toCenter = new THREE.Vector3(0, 0, 0).sub(p.pos); + p.speed.add(toCenter.multiplyScalar(0.0002)); + + // Speed limits + p.speed.clampLength(0.005, 0.04); + + // Boundaries wrap-around + if (p.pos.y < -4) { + p.pos.y = 4; + p.pos.x = (Math.random() - 0.5) * 6; + } + if (p.pos.x < -6) p.pos.x = 6; + if (p.pos.x > 6) p.pos.x = -6; + + child.position.copy(p.pos); + child.rotation.set(p.rot.x, p.rot.y, p.rot.z); + }); + }); + + return ( + + {Array.from({ length: count }).map((_, idx) => { + const isCurryLeaf = idx % 3 === 0; + const isRedChili = idx % 3 === 1; + + if (isCurryLeaf) { + // Curved plane for Curry Leaf + return ( + + + + + ); + } else if (isRedChili) { + // Cylinder for chili pod + return ( + + + + + ); + } else { + // Sphere for Mustard Seed + return ( + + + + + ); + } + })} + + ); +} + +// Steam Particles rising from plate +function SteamEffect() { + const count = 15; + const groupRef = useRef(null); + const steamParticles = useRef>([]); + + useEffect(() => { + const temp: typeof steamParticles.current = []; + for (let i = 0; i < count; i++) { + temp.push({ + pos: new THREE.Vector3( + (Math.random() - 0.5) * 1.5, + Math.random() * 2 + 0.2, + (Math.random() - 0.5) * 1.5 + ), + speedY: Math.random() * 0.015 + 0.008, + speedX: (Math.random() - 0.5) * 0.006, + opacity: Math.random() * 0.3 + 0.1, + scale: Math.random() * 0.3 + 0.2, + }); + } + steamParticles.current = temp; + }, []); + + useFrame(() => { + if (!groupRef.current) return; + const children = groupRef.current.children; + steamParticles.current.forEach((p, idx) => { + const child = children[idx] as THREE.Mesh; + if (!child) return; + + p.pos.y += p.speedY; + p.pos.x += p.speedX; + p.opacity -= 0.0018; // Fade out as it rises + + // Reset when faded or high + if (p.pos.y > 3.0 || p.opacity <= 0) { + p.pos.y = Math.random() * 0.2 + 0.15; + p.pos.x = (Math.random() - 0.5) * 1.2; + p.opacity = Math.random() * 0.3 + 0.1; + } + + child.position.copy(p.pos); + child.scale.setScalar(p.scale * (1 + p.pos.y * 0.5)); // expand as it rises + + const mat = child.material as THREE.MeshBasicMaterial; + if (mat) { + mat.opacity = p.opacity; + } + }); + }); + + return ( + + {Array.from({ length: count }).map((_, idx) => ( + + + + + ))} + + ); +} + +export default function FoodCanvas({ activeIdx, isHovered }: { activeIdx: number; isHovered: boolean }) { + return ( +
+ + + + {/* Spotlight shining on plate for royal gold reflections */} + + + + + +
+ + + +
+ + +
+
+ ); +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..ad3bcd7 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { useState } from "react"; +import { Instagram, Facebook, Mail, Phone, MapPin, Send, Compass } from "lucide-react"; + +export default function Footer() { + const [email, setEmail] = useState(""); + const [subscribed, setSubscribed] = useState(false); + + const handleSubscribe = (e: React.FormEvent) => { + e.preventDefault(); + if (!email) return; + setSubscribed(true); + setEmail(""); + }; + + return ( +
+ + {/* Traditional Toran Hanging Garland at the very top of footer */} +
+ + {/* Floating Diyas (Lamps) floating gently */} +
+ {/* Diya 1 */} +
+
+
+ + {/* Diya 2 */} +
+
+
+
+ +
+ + {/* Col 1: Brand & Bio */} +
+
+ MY DOSA + PLACE +
+
+ +

+ Waterloo's premier destination for authentic, pure vegetarian South Indian flavors. Crispy ghee-roasted dosas, steaming lentil sambar, and home-ground chutneys crafted with devotion. +

+ + {/* Social Icons */} + +
+ + {/* Col 2: Opening Hours */} +
+

+ Feast Sessions +

+
    +
  • + Wednesday – Monday + 04:30 PM – 10:00 PM +
  • +
  • + Tuesday + Closed +
  • +
+
+ + {/* Col 3: Contact & Address */} +
+

+ The Dining Court +

+
    +
  • + + 922 Erb St. W Unit C3#3, Waterloo, ON N2T 0C9, Canada +
  • +
  • + + +1 519-888-7459 +
  • +
  • + + info@mydosaplace.com +
  • +
+
+ + {/* Col 4: Newsletter */} +
+

+ Invite Dispatch +

+

+ Subscribe to receive exclusive banquet menus and seasonal festival calendars. +

+ + {!subscribed ? ( +
+ setEmail(e.target.value)} + placeholder="Digital address" + className="font-sans text-[10px] text-white bg-darkbg-light border border-gold/25 focus:border-gold px-3 py-2 outline-none rounded flex-1 transition-colors duration-300" + /> + +
+ ) : ( + + Seal Dispatched! + + )} +
+ +
+ + {/* Footer Bottom copyright & credits */} +
+ © 2026 My Dosa Place. All rights reserved. + Pure Veg South Indian Heritage. +
+ + +
+ ); +} diff --git a/src/components/InteriorGallery.tsx b/src/components/InteriorGallery.tsx new file mode 100644 index 0000000..7882a59 --- /dev/null +++ b/src/components/InteriorGallery.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import Image from "next/image"; +import { Compass, Sparkles } from "lucide-react"; + +gsap.registerPlugin(ScrollTrigger); + +export default function InteriorGallery() { + const containerRef = useRef(null); + const trackRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + const track = trackRef.current; + if (!container || !track) return; + + const ctx = gsap.context(() => { + // Calculate translation distance + const trackWidth = track.scrollWidth; + const scrollDistance = trackWidth - window.innerWidth; + + // Horizontal Scroll Trigger + const scrollTween = gsap.to(track, { + x: -scrollDistance, + ease: "none", + scrollTrigger: { + trigger: container, + pin: true, + start: "top top", + end: `+=${scrollDistance}`, // scroll height equals translation distance + scrub: 1, + invalidateOnRefresh: true, + }, + }); + + // Image Zoom Parallax during slide + const images = track.querySelectorAll(".gallery-img"); + images.forEach((img) => { + gsap.fromTo( + img, + { scale: 1.0 }, + { + scale: 1.15, + ease: "none", + scrollTrigger: { + trigger: img, + containerAnimation: scrollTween, // sync with parent horizontal tween + start: "left right", + end: "right left", + scrub: true, + }, + } + ); + }); + }, container); + + return () => ctx.revert(); + }, []); + + const slides = [ + { + title: "The Heritage Tour", + subtitle: "C I N E M A T I C S P A C E S", + desc: "Swipe or scroll to explore the visual spatial transitions of My Dosa Place, bringing heritage pillars, brass lanterns, and set feast tables to life.", + isTitle: true, + }, + { + title: "The Teakwood Columns", + subtitle: "T H E T E A K C O U R T", + desc: "Supported by eighty grand hand-carved pillars sourced from Chettinad, framing a courtyard that captures cooling cross-breezes.", + img: "/images/chettinad_interior.png", + isTitle: false, + }, + { + title: "The Royal Banquet Court", + subtitle: "I M P E R I A L D I N I N G", + desc: "Our ceremonial feast hall features heavy teak tables, soft yellow lanterns, and floral motifs (Kolams) hand-drawn with rice flour daily.", + img: "/images/royal_dining_experience.png", + isTitle: false, + }, + { + title: "The Sanctuary Lobby", + subtitle: "T H E W E L C O M E A N T E R O O M", + desc: "Paved with rough-cut temple stone tiles, featuring bubbling brass vessels (urli) filled with fresh jasmine buds and marigolds.", + img: "/images/south_indian_welcome.png", + isTitle: false, + }, + ]; + + return ( +
+ {/* Scroll Track */} +
+ {slides.map((slide, idx) => ( +
+ {slide.isTitle ? ( + // Slide 1: Welcome / Title Slide +
+ + H E R I T A G E W A L K W A Y + +

+ THE SPATIAL
+ + SANCTUARY + +

+
+

+ {slide.desc} +

+
+ Scroll Down to Slide Right +
+
+
+ ) : ( + // Other Slides: Immersive Image with Glassmorphism overlay +
+ {/* Image layer */} +
+ {slide.title} +
+
+ + {/* Corner ornaments */} +
+
+ + {/* Glassmorphic card overlay */} +
+ + {slide.subtitle} + +

+ {slide.title} +

+

+ {slide.desc} +

+
+
+ )} +
+ ))} +
+ + +
+ ); +} diff --git a/src/components/MenuPreview.tsx b/src/components/MenuPreview.tsx new file mode 100644 index 0000000..5f19ec2 --- /dev/null +++ b/src/components/MenuPreview.tsx @@ -0,0 +1,257 @@ +"use client"; + +import { motion } from "framer-motion"; +import Image from "next/image"; + +export default function MenuPreview() { + const leftColumnItems = [ + { + name: "ONION RAVA DOSA", + desc: "Crispy semolina crepe with onions, herbs & spices.", + tag: "RAVA SPL", + tagType: "green" + }, + { + name: "IDLI SAMBAR (2 PCS)", + desc: "Steamed fluffy rice & lentil cakes with hot sambar.", + tag: "TIFFIN SPL", + tagType: "gold" + }, + { + name: "MYSORE MASALA DOSA", + desc: "Rice crepe spread with spicy chutney & potato masala.", + tag: "CLASSIC", + tagType: "orange" + }, + { + name: "RAVA MASALA DOSA", + desc: "Crispy semolina crepe filled with spiced potato masala.", + tag: "RAVA SPL", + tagType: "green" + }, + { + name: "PURI BHAJI (3 PCS)", + desc: "Golden fluffy deep-fried puris with flavorful potato bhaji.", + tag: "TIFFIN SPL", + tagType: "gold" + } + ]; + + const rightColumnItems = [ + { + name: "MASALA DOSA", + desc: "Crispy thin rice & lentil crepe stuffed with potato masala.", + tag: "CLASSIC", + tagType: "orange" + }, + { + name: "ONION RAVA MASALA DOSA", + desc: "Semolina crepe with onions and spiced potato filling.", + tag: "RAVA SPL", + tagType: "green" + }, + { + name: "MEDU VADA (2 PCS)", + desc: "Deep fried crispy lentil donuts served with sambar & chutney.", + tag: "TIFFIN SPL", + tagType: "gold" + }, + { + name: "PAPER ROAST DOSA", + desc: "Extra large, paper-thin, crispy dosa served hot.", + tag: "CLASSIC", + tagType: "orange" + }, + { + name: "RAVA PLAIN DOSA", + desc: "Crispy semolina crepe served with chutney & sambar.", + tag: "RAVA SPL", + tagType: "green" + } + ]; + + const containerVariants = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.12 + } + } + } as const; + + const itemVariants = { + hidden: { opacity: 0, y: 15 }, + show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } } + } as const; + + return ( +
+ + {/* Background Gopuram sketch (Left) */} + + Gopuram Sketch + + + {/* Background Palace sketch (Right) */} + + Palace Sketch + + +
+ + {/* Header Elements */} +
+ {/* Top traditional gold design element */} +
+
+ +

+ Our Menu +

+ + {/* Double line with diamond ornament */} +
+
+ +
+
+ +

+ Explore a wide variety of authentic South Indian vegetarian dishes,
+ crafted with traditional recipes and the finest ingredients. +

+
+ + {/* 2-Column Menu Layout wrapper */} +
+ + {/* Vertical Dotted Separator in the center (Desktop only) */} +
+ + {/* Left Column Items */} + + {leftColumnItems.map((item, index) => ( + +
+
+ {/* Double Dot Arrow Indicator */} + + ..> + +

+ {item.name} +

+
+

+ {item.desc} +

+
+ + {/* Status Badges on the right */} +
+ + {item.tag} + +
+
+ ))} +
+ + {/* Right Column Items */} + + {rightColumnItems.map((item, index) => ( + +
+
+ {/* Double Dot Arrow Indicator */} + + ..> + +

+ {item.name} +

+
+

+ {item.desc} +

+
+ + {/* Status Badges on the right */} +
+ + {item.tag} + +
+
+ ))} +
+ +
+ +
+
+ ); +} diff --git a/src/components/ReservationForm.tsx b/src/components/ReservationForm.tsx new file mode 100644 index 0000000..61dcff2 --- /dev/null +++ b/src/components/ReservationForm.tsx @@ -0,0 +1,289 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { CalendarDays, Clock, Users, ArrowRight, CheckCircle, Sparkles } from "lucide-react"; + +export default function ReservationForm() { + const canvasRef = useRef(null); + const [formData, setFormData] = useState({ + name: "", + email: "", + guests: "2", + date: "", + time: "19:00", + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + + // Sparks/Gold embers canvas particle system + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let animId: number; + let width = (canvas.width = canvas.parentElement?.clientWidth || 800); + let height = (canvas.height = canvas.parentElement?.clientHeight || 600); + + const handleResize = () => { + if (!canvas.parentElement) return; + width = canvas.width = canvas.parentElement.clientWidth; + height = canvas.height = canvas.parentElement.clientHeight; + }; + window.addEventListener("resize", handleResize); + + class Spark { + x: number; + y: number; + vx: number; + vy: number; + radius: number; + alpha: number; + decay: number; + + constructor() { + this.x = Math.random() * width; + this.y = height + 10; + this.vx = (Math.random() - 0.5) * 0.5; + this.vy = -Math.random() * 1.2 - 0.4; + this.radius = Math.random() * 2.0 + 0.5; + this.alpha = Math.random() * 0.6 + 0.2; + this.decay = Math.random() * 0.003 + 0.001; + } + + update() { + this.x += this.vx; + this.y += this.vy; + this.alpha -= this.decay; + this.vx += (Math.random() - 0.5) * 0.05; + } + + draw(c: CanvasRenderingContext2D) { + c.save(); + c.beginPath(); + c.fillStyle = `rgba(212, 175, 55, ${this.alpha})`; + c.shadowBlur = 6; + c.shadowColor = "#D4AF37"; + c.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + c.fill(); + c.restore(); + } + } + + const sparks: Spark[] = []; + const maxSparks = 35; + + const render = () => { + ctx.clearRect(0, 0, width, height); + + if (sparks.length < maxSparks && Math.random() > 0.6) { + sparks.push(new Spark()); + } + + for (let i = sparks.length - 1; i >= 0; i--) { + const s = sparks[i]; + s.update(); + s.draw(ctx); + + if (s.alpha <= 0 || s.y < 0) { + sparks.splice(i, 1); + } + } + + animId = requestAnimationFrame(render); + }; + + render(); + + return () => { + window.removeEventListener("resize", handleResize); + cancelAnimationFrame(animId); + }; + }, []); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + + // Simulate luxury API booking delay + setTimeout(() => { + setIsSubmitting(false); + setIsSuccess(true); + }, 2000); + }; + + return ( +
+ {/* Background Spark Canvas */} + + +
+ + {/* Title */} +
+ + S A N C T U A R Y T A B L E + +

+ RESERVE A COURT +

+
+
+ + {/* Reservation Card wrapper */} +
+ + {/* Inner ornaments */} +
+
+ + {/* Form states */} + {!isSuccess ? ( +
+ + {/* Form Grid */} +
+ + {/* Name */} +
+ + setFormData({ ...formData, name: e.target.value })} + className="font-sans text-xs md:text-sm text-stone-900 bg-white border border-gold/30 focus:border-maroon px-4 py-3 outline-none transition-all duration-300 rounded" + placeholder="Enter your full name" + /> +
+ + {/* Email */} +
+ + setFormData({ ...formData, email: e.target.value })} + className="font-sans text-xs md:text-sm text-stone-900 bg-white border border-gold/30 focus:border-maroon px-4 py-3 outline-none transition-all duration-300 rounded" + placeholder="name@domain.com" + /> +
+ + {/* Guest Count */} +
+ + +
+ + {/* Date */} +
+ + setFormData({ ...formData, date: e.target.value })} + className="font-sans text-xs md:text-sm text-stone-900 bg-white border border-gold/30 focus:border-maroon px-4 py-3 outline-none transition-all duration-300 rounded cursor-pointer" + /> +
+ + {/* Time */} +
+ + +
+ +
+ + {/* Submit CTA */} + + +
+ ) : ( + // Success Feedback State +
+ +
+

+ COURT SECURED +

+ + N A L V A R A V U P A T R O N + +

+ Namaste, {formData.name}. Your royal table for{" "} + {formData.guests} guests on{" "} + {formData.date} at{" "} + {formData.time} has been provisionally registered. A digital invite seal has been dispatched to{" "} + {formData.email}. We wait to welcome you. +

+
+ + +
+ )} + +
+ +
+
+ ); +} diff --git a/src/components/RestaurantStory.tsx b/src/components/RestaurantStory.tsx new file mode 100644 index 0000000..05f61ab --- /dev/null +++ b/src/components/RestaurantStory.tsx @@ -0,0 +1,256 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { motion, useScroll, useTransform } from "framer-motion"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; +import Image from "next/image"; + +gsap.registerPlugin(ScrollTrigger); + +export default function RestaurantStory() { + const sectionRef = useRef(null); + const parchmentRef = useRef(null); + const titleRef = useRef(null); + + // Framer Motion scroll tracking for simple parallax of background and frames + const { scrollYProgress } = useScroll({ + target: sectionRef, + offset: ["start end", "end start"], + }); + + const yBackground = useTransform(scrollYProgress, [0, 1], ["-10%", "10%"]); + const yImage1 = useTransform(scrollYProgress, [0, 1], ["0%", "-15%"]); + const yImage2 = useTransform(scrollYProgress, [0, 1], ["10%", "-5%"]); + + useEffect(() => { + const parchment = parchmentRef.current; + if (!parchment) return; + + const ctx = gsap.context(() => { + // Vintage paper reveal: expands vertically like a scroll unrolling + gsap.fromTo( + parchment, + { + clipPath: "polygon(0 48%, 100% 48%, 100% 52%, 0 52%)", + scale: 0.95, + }, + { + clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)", + scale: 1, + duration: 2, + ease: "power3.inOut", + scrollTrigger: { + trigger: parchment, + start: "top 80%", + end: "top 30%", + scrub: true, + }, + } + ); + + // Text ink reveal: lines fade and spread slightly on scroll + const textLines = parchment.querySelectorAll(".ink-reveal"); + textLines.forEach((line) => { + gsap.fromTo( + line, + { opacity: 0.1, filter: "blur(4px)", y: 15 }, + { + opacity: 1, + filter: "blur(0px)", + y: 0, + duration: 1.2, + scrollTrigger: { + trigger: line, + start: "top 85%", + end: "top 60%", + scrub: true, + }, + } + ); + }); + }, parchment); + + return () => ctx.revert(); + }, []); + + const timelineEvents = [ + { + year: "1928", + title: "The Palace Foundation", + text: "Our journey began in a grand Chettinad palace. Built with Burmese teak and Italian marble, the mansion served as a sanctuary of hospitality, welcoming royal emissaries and regional chieftains with legendary feasts.", + }, + { + year: "1972", + title: "Preserving Ancient Spices", + text: "The royal recipes were transcribed from palm leaves. Master spice-blenders preserved the art of hand-pounding cardamoms, cinnamon, and red sun-dried chilies in stone mortars, ensuring the flavors remained pristine.", + }, + { + year: "2026", + title: "The Modern Legacy Sanctuary", + text: "Today, My Dosa Place brings this heritage into the modern era. We unite century-old cooking methodologies with contemporary culinary presentation, inviting you to dine like royalty in Waterloo.", + }, + ]; + + return ( +
+ {/* Traditional Temple Borders */} +
+ +
+ + {/* Left Side: Vintage Parchment Scroll */} +
+
+ {/* Distressed Paper Edges and Watermark */} +
+
+
+ + {/* Brass Corner Accents */} +
+
+
+
+ + {/* Scroll Header */} +
+ + O U R C H R O N I C L E S + +

+ OUR DOSA CHRONICLES +

+
+
+ + {/* Timeline List */} +
+ {timelineEvents.map((event, idx) => ( +
+ {/* Timeline Node Ring */} +
+ + {/* Event Meta */} +
+ + {event.year} + +

+ {event.title} +

+
+ + {/* Event Text */} +

+ {event.text} +

+
+ ))} +
+ + {/* Bottom Seal Decoration */} +
+
+ DR +
+
+
+ +
+
+ + {/* Right Side: Vintage Framed Pictures with Parallax */} +
+ + {/* Framed Image 1: Mansion Detail */} + + {/* Brass Inner Frame Border */} +
+
+ Vintage Teak Columns +
+ {/* Title Tag */} +
+ Circa 1928 +
+ + + {/* Framed Image 2: Gopuram Temple Arch styled layout */} +
+ +
+ Traditional Serving +
+
+ + {/* Title Tag (Placed outside the clip-path container so it isn't cropped) */} +
+ Royal Dining +
+
+ + {/* Background Ambient Glow */} +
+
+ + {/* SVG ClipPath Definition for the Gopuram Temple Arch frame */} + + + + + + + + +
+
+ ); +} diff --git a/src/components/RouteMap.tsx b/src/components/RouteMap.tsx new file mode 100644 index 0000000..e1d8a62 --- /dev/null +++ b/src/components/RouteMap.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useState } from "react"; +import { motion } from "framer-motion"; +import { MapPin, Navigation, Compass, Sparkles } from "lucide-react"; + +export default function RouteMap() { + const [activePin, setActivePin] = useState(null); + + const landmarks = [ + { + id: "airport", + name: "Waterloo Region Airport (YKF)", + shortName: "YKF Airport", + distance: "25 km (28 mins)", + x: 100, + y: 350, + desc: "Direct regional flight connections, linked via Expressway route 85.", + }, + { + id: "university", + name: "University of Waterloo", + shortName: "UWaterloo", + distance: "3 km (5 mins)", + x: 280, + y: 220, + desc: "Academic hub located just east of Erb Street West.", + }, + { + id: "market", + name: "St. Jacobs Farmers' Market", + shortName: "St. Jacobs", + distance: "8 km (10 mins)", + x: 480, + y: 320, + desc: "Historic local farmers' market offering local produce.", + }, + { + id: "restaurant", + name: "My Dosa Place (Waterloo)", + shortName: "My Dosa Place", + distance: "0 km (Destination)", + x: 680, + y: 180, + desc: "Welcome! Waterloo's destination for authentic pure vegetarian South Indian flavors.", + isDestination: true, + }, + ]; + + return ( +
+ + {/* Background grids */} +
+ +
+ + {/* Title */} +
+ + P I L G R I M A G E + +

+ HERITAGE LOCATION MAP +

+
+
+ + {/* Map Canvas Wrapper */} +
+ + {/* Decorative Corner Ornaments */} +
+
+
+
+ + {/* SVG Map Canvas */} + + {/* Grid Coordinates watermark */} + MAP: WATERLOO-2026 + + {/* Stylized River (Vaigai River representation) */} + + + + {/* Stylized Local Highways */} + + + + {/* The Main Route Path (Airport -> Temple -> Station -> Restaurant) */} + + + {/* Animated Golden Chariot/Car moving along route-path */} + + + + + + + + + + {/* Render Landmarks Pins */} + {landmarks.map((pin) => { + const isActive = activePin === pin.id; + + return ( + setActivePin(pin.id)} + onMouseLeave={() => setActivePin(null)} + > + {/* Outer breathing ring */} + + + {/* Inner Pin center */} + + + {/* Landmark Label */} + + {pin.shortName} + + + ); + })} + + + {/* Dynamic interactive information panel overlay */} +
+

+ Access Route Details +

+ + {activePin ? ( + // Active pin details + (() => { + const pin = landmarks.find((l) => l.id === activePin); + return ( +
+
+ {pin?.name} +
+ + Distance: {pin?.distance} + +

+ {pin?.desc} +

+
+ ); + })() + ) : ( + // Default guide text +
+

+ Hover over the glowing nodes on the cartographic map to audit pick-up pathways and transit distances. +

+
+ )} +
+ +
+ +
+ + +
+ ); +} diff --git a/src/components/SignatureFoods.tsx b/src/components/SignatureFoods.tsx new file mode 100644 index 0000000..ddc57b3 --- /dev/null +++ b/src/components/SignatureFoods.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useState } from "react"; +import dynamic from "next/dynamic"; +import { motion, AnimatePresence } from "framer-motion"; +import { Sparkles, ArrowRight, Heart } from "lucide-react"; + +// Dynamically import FoodCanvas to prevent SSR errors (Three.js relies on document/window) +const FoodCanvas = dynamic(() => import("./FoodCanvas"), { + ssr: false, + loading: () => ( +
+
+ Loading 3D Culinary Assets... +
+ ), +}); + +export default function SignatureFoods() { + const [activeIdx, setActiveIdx] = useState(0); + const [isHovered, setIsHovered] = useState(false); + const [liked, setLiked] = useState>({}); + + const foods = [ + { + name: "Rajamannar Ghee Dosa", + tamilName: "நெய் மசாலா தோசை", + price: "₹550", + description: + "A crispy golden crepe spun from stone-ground organic red rice batter, smeared with spiced garlic-chili podi, filled with an aromatic mash of heritage forest potatoes, and roasted with hand-churned A2 cow ghee.", + highlight: "A2 Ghee Roast • Slow Fermented Batter", + ingredients: ["Red Rice", "A2 Ghee", "Garlic Podi", "Spiced Potato Mash"], + }, + { + name: "Chola Samrat Thali", + tamilName: "சோழ சாம்ராட் தட்டு", + price: "₹1,450", + description: + "A grand imperial culinary tribute featuring twenty distinct heirloom delicacies. Served in heavy brass katoris on a fresh banana leaf, incorporating hand-turned aviyal, steaming ponni rice, robust pepper rasam, and elaneer payasam.", + highlight: "Heritage Recipe • Imperial Dining Feast", + ingredients: ["Chettinad Sambar", "Mappillai Samba Rice", "Pepper Rasam", "Elaneer Payasam"], + }, + ]; + + const toggleLike = (idx: number, e: React.MouseEvent) => { + e.stopPropagation(); + setLiked(prev => ({ ...prev, [idx]: !prev[idx] })); + }; + + return ( +
+ + {/* Background Decorative Element */} +
+
+ +
+ + {/* Section Title */} +
+ + S I G N A T U R E M A S T E R P I E C E S + +

+ THE ROYAL TASTES +

+
+
+ +
+ + {/* Left Side: Interactive Foods Card List */} +
+ {foods.map((food, idx) => { + const isActive = activeIdx === idx; + return ( +
setActiveIdx(idx)} + onMouseEnter={() => { + setActiveIdx(idx); + setIsHovered(true); + }} + onMouseLeave={() => setIsHovered(false)} + className={`clickable p-6 md:p-8 rounded-lg transition-all duration-500 border relative group overflow-hidden ${ + isActive + ? "bg-white border-gold/60 shadow-[0_15px_30px_rgba(24,135,84,0.08)]" + : "bg-white/40 border-stone-200 hover:border-gold/40 hover:bg-white" + }`} + > + {/* Decorative glowing gradient border for active */} + {isActive && ( +
+ )} + + {/* Top line with category, price and like */} +
+
+ + {food.highlight} + +

+ {food.name} +

+

+ {food.tamilName} +

+
+ +
+ + {food.price} + + +
+
+ + {/* Body description */} +

+ {food.description} +

+ + {/* Ingredients Tags */} +
+ {food.ingredients.map((ing, i) => ( + + {ing} + + ))} +
+ + {/* Hover visual arrow reveal */} +
+ +
+
+ ); + })} +
+ + {/* Right Side: Interactive 3D WebGL Canvas Plate */} +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ {/* Outer decorative circular rings */} +
+
+ + {/* R3F WebGL Canvas Container */} + +
+
+ +
+ +
+
+ ); +} diff --git a/src/components/SmoothScroll.tsx b/src/components/SmoothScroll.tsx new file mode 100644 index 0000000..1a7abcb --- /dev/null +++ b/src/components/SmoothScroll.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect } from "react"; +import Lenis from "lenis"; +import gsap from "gsap"; +import { ScrollTrigger } from "gsap/ScrollTrigger"; + +// Register ScrollTrigger plugin +gsap.registerPlugin(ScrollTrigger); + +export default function SmoothScroll({ children }: { children: React.ReactNode }) { + useEffect(() => { + // Initialize Lenis + const lenis = new Lenis({ + duration: 1.5, + easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), + orientation: "vertical", + gestureOrientation: "vertical", + smoothWheel: true, + wheelMultiplier: 1, + touchMultiplier: 1.5, + }); + + // Update ScrollTrigger on scroll + const handleScroll = () => { + ScrollTrigger.update(); + }; + + lenis.on("scroll", handleScroll); + + // Connect Lenis frame updates to GSAP ticker + const updateTicker = (time: number) => { + lenis.raf(time * 1000); + }; + + gsap.ticker.add(updateTicker); + gsap.ticker.lagSmoothing(0); + + // Clean up + return () => { + lenis.off("scroll", handleScroll); + lenis.destroy(); + gsap.ticker.remove(updateTicker); + }; + }, []); + + return <>{children}; +} diff --git a/src/components/SoundManager.tsx b/src/components/SoundManager.tsx new file mode 100644 index 0000000..b48a0ca --- /dev/null +++ b/src/components/SoundManager.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useEffect, useState, useRef } from "react"; +import { Volume2, VolumeX } from "lucide-react"; + +export default function SoundManager() { + const [isPlaying, setIsPlaying] = useState(false); + const audioCtxRef = useRef(null); + + // Refs for oscillators and gain nodes to control the drone + const droneNodesRef = useRef<{ + oscillators: OscillatorNode[]; + gains: GainNode[]; + lfo: OscillatorNode | null; + }>({ oscillators: [], gains: [], lfo: null }); + + // Web Audio Tanpura Drone Synthesizer + const startDrone = () => { + if (!audioCtxRef.current) { + audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)(); + } + + const ctx = audioCtxRef.current; + if (ctx.state === "suspended") { + ctx.resume(); + } + + const masterGain = ctx.createGain(); + masterGain.gain.setValueAtTime(0.08, ctx.currentTime); // Soft, unobtrusive volume + masterGain.connect(ctx.destination); + + // Warm Lowpass filter to simulate wood resonance + const filter = ctx.createBiquadFilter(); + filter.type = "lowpass"; + filter.frequency.setValueAtTime(350, ctx.currentTime); + filter.connect(masterGain); + + const freqs = [65.41, 98.0, 130.81, 196.0]; // C2, G2, C3, G3 - traditional Indian scale tuning + const oscillators: OscillatorNode[] = []; + const gains: GainNode[] = []; + + // Create harmonically rich drone oscillators + freqs.forEach((freq, idx) => { + const osc = ctx.createOscillator(); + // Mix sawtooth and triangle for rich but warm string timbre + osc.type = idx % 2 === 0 ? "triangle" : "sine"; + osc.frequency.setValueAtTime(freq, ctx.currentTime); + + const gain = ctx.createGain(); + // Stagger volumes + gain.gain.setValueAtTime(idx === 0 ? 0.4 : 0.25, ctx.currentTime); + + osc.connect(gain); + gain.connect(filter); + + osc.start(); + oscillators.push(osc); + gains.push(gain); + }); + + // Create LFO (Low Frequency Oscillator) to model the rhythmic strumming (swells) of a tanpura + const lfo = ctx.createOscillator(); + lfo.frequency.setValueAtTime(0.12, ctx.currentTime); // Very slow swell (every 8 seconds) + + const lfoGain = ctx.createGain(); + lfoGain.gain.setValueAtTime(0.15, ctx.currentTime); + + lfo.connect(lfoGain); + // Connect LFO to modulate filter cutoff frequency to create organic breathing texture + lfoGain.connect(filter.frequency); + lfo.start(); + + droneNodesRef.current = { oscillators, gains, lfo }; + }; + + const stopDrone = () => { + const { oscillators, lfo } = droneNodesRef.current; + oscillators.forEach((osc) => { + try { osc.stop(); } catch (e) {} + }); + if (lfo) { + try { lfo.stop(); } catch (e) {} + } + droneNodesRef.current = { oscillators: [], gains: [], lfo: null }; + }; + + // Synthesize a traditional temple bell chime + const playBellChime = () => { + if (!audioCtxRef.current || audioCtxRef.current.state === "suspended") return; + + const ctx = audioCtxRef.current; + const now = ctx.currentTime; + + // Bell tones consist of a fundamental frequency and inharmonic overtones + const frequencies = [600, 770, 930, 1200, 1540]; + const bellGain = ctx.createGain(); + bellGain.gain.setValueAtTime(0, now); + // Sudden attack, long delay + bellGain.gain.linearRampToValueAtTime(0.04, now + 0.05); + bellGain.gain.exponentialRampToValueAtTime(0.0001, now + 4.5); + bellGain.connect(ctx.destination); + + frequencies.forEach((f, index) => { + const osc = ctx.createOscillator(); + osc.type = "sine"; + osc.frequency.setValueAtTime(f, now); + // Detune slightly for realistic metallic resonance + osc.detune.setValueAtTime(index * 4, now); + osc.connect(bellGain); + osc.start(now); + osc.stop(now + 5.0); + }); + }; + + const toggleSound = () => { + if (isPlaying) { + stopDrone(); + setIsPlaying(false); + } else { + startDrone(); + setIsPlaying(true); + // Play introductory bell chime + setTimeout(() => { + playBellChime(); + }, 500); + } + }; + + // Periodically ring temple bell (every 22 seconds) for ambient realism + useEffect(() => { + if (!isPlaying) return; + + const interval = setInterval(() => { + playBellChime(); + }, 22000); + + return () => clearInterval(interval); + }, [isPlaying]); + + // Stop sound on unmount + useEffect(() => { + return () => { + stopDrone(); + }; + }, []); + + return ( +
+ {/* Soundwave animation */} + {isPlaying && ( +
+
+
+
+
+
+ )} + + {/* Sound Toggle Button */} + + + {/* Tailwind Soundwave Keyframes definition embedded in a style block */} + +
+ ); +} diff --git a/src/components/Testimonials.tsx b/src/components/Testimonials.tsx new file mode 100644 index 0000000..b3249e3 --- /dev/null +++ b/src/components/Testimonials.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useState, useRef } from "react"; +import { motion } from "framer-motion"; +import { Star, Quote, Sparkles } from "lucide-react"; + +export default function Testimonials() { + const containerRef = useRef(null); + + // Testimonials database + const reviews = [ + { + name: "Aishwarya Krishnan", + role: "Culinary Critic, Madras Digest", + quote: "My Dosa Place is not merely a restaurant; it is a culinary sanctuary. The ghee roast dosa had the exact golden ghee crunch that I've only ever tasted in heritage kitchens. The visual focus onto our set table completes this extraordinary sensory theatre.", + rating: 5, + }, + { + name: "Dr. Raghavan Pillai", + role: "Heritage Historian", + quote: "Dining here felt like attending a grand royal feast in a Chola palace. The attention to traditional serving etiquette, the stone-paved floor, and the hand-ground masalas are a testament to true preservation of culture.", + rating: 5, + }, + { + name: "Siddharth Malhotra", + role: "Luxury Connoisseur", + quote: "The WebGL 3D interface was impressive, but the real-life experience surpassed it. The Chef's storytelling through spices, the live flute tunes, and the incredible banana-leaf thali is unmatched. A true 5-star experience.", + rating: 5, + }, + ]; + + // 3D Card Hover Tilt logic (programmatic per card) + const Card = ({ review }: { review: typeof reviews[0] }) => { + const cardRef = useRef(null); + const [rotateX, setRotateX] = useState(0); + const [rotateY, setRotateY] = useState(0); + + const handleMouseMove = (e: React.MouseEvent) => { + const card = cardRef.current; + if (!card) return; + const rect = card.getBoundingClientRect(); + const width = rect.width; + const height = rect.height; + const mouseX = e.clientX - rect.left - width / 2; + const mouseY = e.clientY - rect.top - height / 2; + + // Calculate rotation angles (max 12 degrees) + const rX = -(mouseY / (height / 2)) * 10; + const rY = (mouseX / (width / 2)) * 10; + + setRotateX(rX); + setRotateY(rY); + }; + + const handleMouseLeave = () => { + setRotateX(0); + setRotateY(0); + }; + + return ( +
+ + {/* Decorative Corner Borders */} +
+
+
+
+ + {/* Background Ambient Glow */} +
+ + {/* Quote Icon */} +
+
+ {Array.from({ length: review.rating }).map((_, i) => ( + + ))} +
+ +
+ + {/* Review Text */} +

+ "{review.quote}" +

+ + {/* Reviewer Details */} +
+ + {review.name} + + + {review.role} + +
+ +
+ ); + }; + + return ( +
+ {/* Background Graphic elements */} +
+
+ +
+ + {/* Title */} +
+ + H O N O R E D P A T R O N S + +

+ GUEST CRITICS & DIARIES +

+
+
+ + {/* Reviews Cards Grid */} +
+ {reviews.map((review, idx) => ( + + ))} +
+ +
+
+ ); +}