├── .env ├── .gitattributes ├── public ├── cover.png ├── spot.png ├── preview-image.png ├── publi-cover-end.png ├── publi-cover-mid.png ├── robots.txt ├── sitemap.xml ├── site.webmanifest └── vite.svg ├── postcss.config.js ├── src ├── assets │ ├── images │ │ ├── M4.jpg │ │ ├── m2.jpg │ │ ├── m3.jpg │ │ ├── music.jpg │ │ ├── spot.png │ │ └── skullemoji.jpg │ ├── fonts │ │ ├── GothamBold.ttf │ │ ├── GothamBook.ttf │ │ ├── GothamMedium.ttf │ │ ├── azonix │ │ │ └── Azonix.otf │ │ ├── babell │ │ │ └── BabellBold.ttf │ │ ├── creato │ │ │ ├── CreatoDisplay-Black.otf │ │ │ ├── CreatoDisplay-Bold.otf │ │ │ ├── CreatoDisplay-Light.otf │ │ │ ├── CreatoDisplay-Thin.otf │ │ │ ├── CreatoDisplay-Medium.otf │ │ │ ├── CreatoDisplay-Regular.otf │ │ │ ├── CreatoDisplay-BoldItalic.otf │ │ │ ├── CreatoDisplay-ExtraBold.otf │ │ │ ├── CreatoDisplay-ThinItalic.otf │ │ │ ├── CreatoDisplay-BlackItalic.otf │ │ │ ├── CreatoDisplay-LightItalic.otf │ │ │ ├── CreatoDisplay-MediumItalic.otf │ │ │ ├── CreatoDisplay-ExtraBoldItalic.otf │ │ │ └── CreatoDisplay-RegularItalic.otf │ │ └── cocosharp │ │ │ ├── Coco-Sharp-Bold-trial.ttf │ │ │ ├── Coco-Sharp-Heavy-trial.ttf │ │ │ ├── Coco-Sharp-Italic-trial.ttf │ │ │ ├── Coco-Sharp-Light-trial.ttf │ │ │ ├── Coco-Sharp-by-zetafonts.png │ │ │ ├── Coco-Sharp-Extrabold-trial.ttf │ │ │ ├── Coco-Sharp-Regular-trial.ttf │ │ │ ├── Coco-Sharp-Bold-Italic-trial.ttf │ │ │ ├── Coco-Sharp-Extralight-trial.ttf │ │ │ ├── Coco-Sharp-Heavy-Italic-trial.ttf │ │ │ ├── Coco-Sharp-Light-Italic-trial.ttf │ │ │ ├── Coco-Sharp-Extrabold-Italic-trial.ttf │ │ │ ├── Coco-Sharp-Extralight-Italic-trial.ttf │ │ │ └── Coco-Sharp-Family-CC-BY-NCLicensepdf.pdf │ ├── index.js │ └── react.svg ├── lib │ └── utils.ts ├── main.jsx ├── App.css ├── components │ ├── GridCard.jsx │ ├── UserProfile.jsx │ └── ui │ │ ├── aurora-background.tsx │ │ ├── following-pointer.tsx │ │ ├── lamp.tsx │ │ ├── glowing-effect.tsx │ │ ├── background-beams.tsx │ │ └── sparkles.tsx ├── utils │ └── spotifyApi.js ├── index.css └── App.jsx ├── tsconfig.json ├── .gitignore ├── vite.config.js ├── components.json ├── generate-sitemap.js ├── eslint.config.js ├── package.json ├── tailwind.config.js ├── README.md └── index.html /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=https://authback-jxx5.onrender.com/api/users -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/public/cover.png -------------------------------------------------------------------------------- /public/spot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/public/spot.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/preview-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/public/preview-image.png -------------------------------------------------------------------------------- /src/assets/images/M4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/images/M4.jpg -------------------------------------------------------------------------------- /src/assets/images/m2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/images/m2.jpg -------------------------------------------------------------------------------- /src/assets/images/m3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/images/m3.jpg -------------------------------------------------------------------------------- /public/publi-cover-end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/public/publi-cover-end.png -------------------------------------------------------------------------------- /public/publi-cover-mid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/public/publi-cover-mid.png -------------------------------------------------------------------------------- /src/assets/images/music.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/images/music.jpg -------------------------------------------------------------------------------- /src/assets/images/spot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/images/spot.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Sitemap: https://thinakaranmanokaran.github.io/spotmix-downloader/sitemap.xml 5 | -------------------------------------------------------------------------------- /src/assets/fonts/GothamBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/GothamBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/GothamBook.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/GothamBook.ttf -------------------------------------------------------------------------------- /src/assets/images/skullemoji.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/images/skullemoji.jpg -------------------------------------------------------------------------------- /src/assets/fonts/GothamMedium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/GothamMedium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/azonix/Azonix.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/azonix/Azonix.otf -------------------------------------------------------------------------------- /src/assets/fonts/babell/BabellBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/babell/BabellBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-Black.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-Bold.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-Light.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-Thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-Thin.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-Medium.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-Regular.otf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Bold-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Bold-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-BoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-BoldItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-ExtraBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-ExtraBold.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-ThinItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-ThinItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Heavy-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Heavy-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Italic-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Italic-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Light-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Light-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-by-zetafonts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-by-zetafonts.png -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-BlackItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-BlackItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-LightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-LightItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-MediumItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-MediumItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Extrabold-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Extrabold-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Regular-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Regular-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-ExtraBoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-ExtraBoldItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/creato/CreatoDisplay-RegularItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/creato/CreatoDisplay-RegularItalic.otf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Bold-Italic-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Bold-Italic-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Extralight-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Extralight-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Heavy-Italic-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Heavy-Italic-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Light-Italic-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Light-Italic-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Extrabold-Italic-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Extrabold-Italic-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Extralight-Italic-trial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Extralight-Italic-trial.ttf -------------------------------------------------------------------------------- /src/assets/fonts/cocosharp/Coco-Sharp-Family-CC-BY-NCLicensepdf.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinakaranmanokaran/spotmix-downloader/HEAD/src/assets/fonts/cocosharp/Coco-Sharp-Family-CC-BY-NCLicensepdf.pdf -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": [ 6 | "src/*" 7 | ] 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/assets/index.js: -------------------------------------------------------------------------------- 1 | import MusicDoodle1 from "./images/music.jpg" 2 | import MusicDoodle2 from "./images/m2.jpg" 3 | import MusicDoodle3 from "./images/m3.jpg" 4 | import MusicDoodle4 from "./images/M4.jpg" 5 | 6 | export default { 7 | MusicDoodle1, 8 | MusicDoodle2, 9 | MusicDoodle3, 10 | MusicDoodle4, 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .vite 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | https://thinakaranmanokaran.github.io/2025-08-14T00:00:00.000Zweekly1.0 -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import { defineConfig } from "vite"; 3 | import react from "@vitejs/plugin-react"; 4 | import { webcrypto } from "node:crypto"; // ✅ ESM import 5 | 6 | // Polyfill for Node build environment 7 | if (!globalThis.crypto) { 8 | globalThis.crypto = webcrypto; 9 | } 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | plugins: [react()], 14 | base: "/spotmix-downloader/", 15 | build: { 16 | outDir: 'dist', // Ensure correct output folder 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spotify Song Downloader", 3 | "short_name": "Spotify Downloader", 4 | "description": "Download any Spotify song, playlist, or album as high-quality MP3 instantly by pasting the Spotify URL. 100% free, no premium needed.", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#000000", 8 | "theme_color": "#1db954", 9 | "icons": [ 10 | { 11 | "src": "/spot.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/spot.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /generate-sitemap.js: -------------------------------------------------------------------------------- 1 | // generate-sitemap.js 2 | import { writeFileSync } from 'fs'; 3 | import { SitemapStream, streamToPromise } from 'sitemap'; 4 | import { resolve } from 'path'; 5 | 6 | const baseUrl = 'https://thinakaranmanokaran.github.io/spotmix-downloader/'; 7 | 8 | // List of site pages (you can add more later) 9 | const pages = [ 10 | { url: '/', changefreq: 'weekly', priority: 1.0 }, 11 | ]; 12 | 13 | (async () => { 14 | const sitemapStream = new SitemapStream({ hostname: baseUrl }); 15 | 16 | pages.forEach(page => { 17 | sitemapStream.write({ 18 | url: page.url, 19 | changefreq: page.changefreq, 20 | priority: page.priority, 21 | lastmod: new Date().toISOString().split('T')[0], // auto-update 22 | }); 23 | }); 24 | 25 | sitemapStream.end(); 26 | 27 | const sitemapData = await streamToPromise(sitemapStream); 28 | writeFileSync(resolve('./public/sitemap.xml'), sitemapData.toString()); 29 | 30 | console.log('✅ Sitemap generated at public/sitemap.xml'); 31 | })(); 32 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /src/components/GridCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { motion } from "motion/react"; 3 | 4 | const GridCard = ({ number, title, description, className }) => { 5 | return ( 6 | 13 |
+
14 |
+
15 |
+
16 |
+
17 |
{number}
18 |
{title}
19 |
{description}
20 |
21 | ) 22 | } 23 | 24 | export default GridCard -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spot-mix", 3 | "private": true, 4 | "version": "2.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "node generate-sitemap.js && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "deploy": "gh-pages -d dist" 12 | }, 13 | "dependencies": { 14 | "@tsparticles/engine": "^3.9.1", 15 | "@tsparticles/react": "^3.0.0", 16 | "@tsparticles/slim": "^3.9.1", 17 | "axios": "^1.7.9", 18 | "class-variance-authority": "^0.7.1", 19 | "clsx": "^2.1.1", 20 | "jwt-decode": "^4.0.0", 21 | "lucide-react": "^0.539.0", 22 | "motion": "^12.23.12", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "tailwind-merge": "^3.3.1", 26 | "tailwindcss-animate": "^1.0.7" 27 | }, 28 | "devDependencies": { 29 | "@eslint/js": "^9.11.1", 30 | "@types/react": "^18.3.10", 31 | "@types/react-dom": "^18.3.0", 32 | "@vitejs/plugin-react": "^4.3.2", 33 | "autoprefixer": "^10.4.20", 34 | "eslint": "^9.11.1", 35 | "eslint-plugin-react": "^7.37.0", 36 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 37 | "eslint-plugin-react-refresh": "^0.4.12", 38 | "gh-pages": "^6.3.0", 39 | "globals": "^15.9.0", 40 | "postcss": "^8.4.47", 41 | "sitemap": "^8.0.0", 42 | "tailwindcss": "^3.4.13", 43 | "vite": "^7.1.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/spotifyApi.js: -------------------------------------------------------------------------------- 1 | // spotifyApi.js 2 | export async function getAccessToken() { 3 | const clientId = import.meta.env.VITE_SPOTIFY_CLIENT; 4 | const clientSecret = import.meta.env.VITE_SPOTIFY_SECRET; 5 | 6 | console.log('Client ID:', import.meta.env.VITE_SPOTIFY_CLIENT); 7 | console.log('Client Secret exists:', Boolean(import.meta.env.VITE_SPOTIFY_SECRET)); 8 | 9 | // Basic check if credentials exist 10 | if (!clientId || !clientSecret) { 11 | throw new Error('Spotify client credentials not configured'); 12 | } 13 | 14 | try { 15 | // Create the authorization header 16 | const authHeader = 'Basic ' + btoa(`${clientId}:${clientSecret}`); 17 | 18 | const response = await fetch("https://accounts.spotify.com/api/token", { 19 | method: "POST", 20 | headers: { 21 | "Content-Type": "application/x-www-form-urlencoded", 22 | "Authorization": authHeader, 23 | }, 24 | body: new URLSearchParams({ 25 | grant_type: 'client_credentials' 26 | }) 27 | }); 28 | 29 | if (!response.ok) { 30 | const errorData = await response.json(); 31 | throw new Error(`Spotify API error: ${errorData.error_description || response.statusText}`); 32 | } 33 | 34 | const data = await response.json(); 35 | return data.access_token; 36 | } catch (error) { 37 | console.error('Error getting access token:', error); 38 | throw error; 39 | } 40 | } -------------------------------------------------------------------------------- /src/components/UserProfile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import axios from 'axios'; 3 | import Skull from './../assets/images/skullemoji.jpg' 4 | 5 | 6 | const UserProfile = ({ currentUser }) => { 7 | 8 | const [userProfile, setUserProfile] = useState(false); 9 | 10 | function showProfile() { 11 | setUserProfile(!userProfile); 12 | } 13 | 14 | const UserDetails = ({ currentUser }) => { 15 | return( 16 |
17 |
18 |
19 | {currentUser.email} 20 |
21 |
22 |
23 | ) 24 | } 25 | 26 | return ( 27 |
28 |
29 | { 30 | currentUser ? ( 31 |
32 |

33 | 34 |

35 | { userProfile && } 36 |
37 | ) : (

) 38 | } 39 |
40 |
41 | ) 42 | } 43 | 44 | export default UserProfile -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | darkMode: ["class"], 4 | content: [ 5 | "./index.html", 6 | "./src/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | extend: { 10 | fontFamily: { 11 | spotify: [ 12 | 'spotify', 13 | 'sans-serif' 14 | ], 15 | bigfont: [ 16 | 'BigFont', 17 | 'sans-serif' 18 | ], 19 | spotifyMed: [ 20 | 'spotifyMed', 21 | 'sans-serif' 22 | ], 23 | number: [ 24 | 'number', 25 | 'sans-serif' 26 | ], 27 | para: [ 28 | 'Para', 29 | 'sans-serif' 30 | ], 31 | title: [ 32 | 'Title', 33 | 'sans-serif' 34 | ] 35 | }, 36 | borderRadius: { 37 | lg: 'var(--radius)', 38 | md: 'calc(var(--radius) - 2px)', 39 | sm: 'calc(var(--radius) - 4px)' 40 | }, 41 | colors: { 42 | background: 'hsl(var(--background))', 43 | foreground: 'hsl(var(--foreground))', 44 | card: { 45 | DEFAULT: 'hsl(var(--card))', 46 | foreground: 'hsl(var(--card-foreground))' 47 | }, 48 | popover: { 49 | DEFAULT: 'hsl(var(--popover))', 50 | foreground: 'hsl(var(--popover-foreground))' 51 | }, 52 | primary: { 53 | DEFAULT: 'hsl(var(--primary))', 54 | foreground: 'hsl(var(--primary-foreground))' 55 | }, 56 | secondary: { 57 | DEFAULT: 'hsl(var(--secondary))', 58 | foreground: 'hsl(var(--secondary-foreground))' 59 | }, 60 | muted: { 61 | DEFAULT: 'hsl(var(--muted))', 62 | foreground: 'hsl(var(--muted-foreground))' 63 | }, 64 | accent: { 65 | DEFAULT: 'hsl(var(--accent))', 66 | foreground: 'hsl(var(--accent-foreground))' 67 | }, 68 | destructive: { 69 | DEFAULT: 'hsl(var(--destructive))', 70 | foreground: 'hsl(var(--destructive-foreground))' 71 | }, 72 | border: 'hsl(var(--border))', 73 | input: 'hsl(var(--input))', 74 | ring: 'hsl(var(--ring))', 75 | chart: { 76 | '1': 'hsl(var(--chart-1))', 77 | '2': 'hsl(var(--chart-2))', 78 | '3': 'hsl(var(--chart-3))', 79 | '4': 'hsl(var(--chart-4))', 80 | '5': 'hsl(var(--chart-5))' 81 | } 82 | } 83 | } 84 | }, 85 | plugins: [require("tailwindcss-animate")], 86 | } 87 | -------------------------------------------------------------------------------- /src/components/ui/aurora-background.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { cn } from "../../lib/utils"; 3 | import React, { ReactNode } from "react"; 4 | 5 | interface AuroraBackgroundProps extends React.HTMLProps { 6 | children: ReactNode; 7 | showRadialGradient?: boolean; 8 | } 9 | 10 | export const AuroraBackground = ({ 11 | className, 12 | children, 13 | showRadialGradient = true, 14 | ...props 15 | }: AuroraBackgroundProps) => { 16 | return ( 17 |
18 |
25 |
47 |
56 |
57 | {children} 58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 🎵 Spotify Downloader – Download Songs, Playlists & Albums in MP3 4 | 5 | **Spotify Downloader** is a blazing-fast React web application that lets you **download Spotify songs, playlists, and albums** directly in **high-quality MP3, AAC, or FLAC formats**. 6 | No Spotify Premium required. Preserve **album artwork, metadata, and original audio quality** – all for free. 7 | 8 | 9 | ## 🖼 Screenshot 10 | 11 | ![Spotify Downloader Screenshot](./public/cover.png) 12 | 13 | --- 14 | 15 | ## 🚀 Features 16 | 17 | - **Download Any Spotify Song** – Save your favorite tracks instantly. 18 | - **Playlist & Album Support** – Batch download entire collections. 19 | - **High-Quality Audio** – Get up to **320kbps MP3** or **lossless formats**. 20 | - **No Premium Required** – Works with a free Spotify account. 21 | - **Lightning-Fast** – Optimized servers for ultra-quick conversions. 22 | - **Full Metadata** – Keep song title, artist, release year, and album cover. 23 | 24 | --- 25 | 26 | ## 🌍 Live Demo 27 | 28 | Try the live version here: 29 | [🔗 Spotify Downloader – Live](https://thinakaranmanokaran.github.io/spotmix-downloader/) 30 | 31 | --- 32 | 33 | 34 | ## 📦 Installation & Setup 35 | 36 | ### Prerequisites 37 | - **Node.js** (LTS recommended) 38 | - **npm** or **yarn** 39 | 40 | ### Steps 41 | 42 | ```bash 43 | # 1. Clone the repository 44 | git clone https://github.com/thinakaranmanokaran/spotify-downloader.git 45 | cd spotify-downloader 46 | 47 | # 2. Install dependencies 48 | npm install # or yarn install 49 | 50 | # 3. Start the development server 51 | npm start # or yarn start 52 | ```` 53 | 54 | Visit **[http://localhost:3000](http://localhost:3000)** in your browser. 55 | 56 | --- 57 | 58 | ## 📥 How to Use 59 | 60 | 1. **Copy** any Spotify track, playlist, or album URL. 61 | 2. **Paste** the link into the input field on the website. 62 | 3. **Verify** to fetch details like album name, cover image, artist, and release date. 63 | 4. **Download** the song(s) instantly in your preferred format. 64 | 65 | --- 66 | 67 | ## 🚀 Deployment (GitHub Pages) 68 | 69 | 1. In `package.json`, set the `homepage`: 70 | 71 | ```json 72 | "homepage": "https://thinakaranmanokaran.github.io/spotmix-downloader/" 73 | ``` 74 | 2. Run: 75 | 76 | ```bash 77 | npm run deploy 78 | ``` 79 | 3. Your site will be live on GitHub Pages. 80 | 81 | --- 82 | 83 | ## 🛠 Tech Stack 84 | 85 | * **React.js** – Frontend framework 86 | * **Tailwind CSS** – Styling 87 | * **Spotify Downloader API** – Fetches song details and download links 88 | * **GitHub Pages** – Deployment 89 | 90 | --- 91 | 92 | ## 🤝 Contributing 93 | 94 | Contributions are welcome! 95 | Follow these steps: 96 | 97 | ```bash 98 | # 1. Fork the repo 99 | # 2. Create a branch 100 | git checkout -b feature-branch 101 | 102 | # 3. Commit your changes 103 | git commit -m "Add new feature" 104 | 105 | # 4. Push and submit PR 106 | git push origin feature-branch 107 | ``` 108 | 109 | --- 110 | 111 | ## 📜 License 112 | 113 | This project is licensed under the **MIT License** – see the [LICENSE](LICENSE) file. 114 | 115 | --- 116 | 117 | ## 😇 Author 118 | 119 | [Thinakaran Manokaran](https://thinakaran.dev) 120 | 121 | --- 122 | 123 | ### 📈 SEO Keywords 124 | 125 | 126 | ``` 127 | 128 | Spotify downloader, Spotify song download, download Spotify playlist MP3, Spotify to MP3, free Spotify music downloader, download Spotify album, Spotify MP3 converter, download Spotify without premium 129 | 130 | ``` 131 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: spotify; 7 | src: url('./assets/fonts/GothamBold.ttf'); 8 | } 9 | 10 | @font-face { 11 | font-family: spotifyMed; 12 | src: url('./assets/fonts/GothamBook.ttf'); 13 | } 14 | 15 | body{ 16 | background-color: black; 17 | } 18 | 19 | body::-webkit-scrollbar { 20 | display: none; 21 | } 22 | 23 | @font-face { 24 | font-family: BigFont; 25 | src: url('./assets/fonts/babell/BabellBold.ttf'); 26 | } 27 | 28 | @font-face { 29 | font-family: number; 30 | src: url(assets/fonts/azonix/Azonix.otf); 31 | } 32 | 33 | @font-face { 34 | font-family: Para; 35 | src: url('./assets/fonts/creato/CreatoDisplay-Light.otf'); 36 | } 37 | 38 | @font-face { 39 | font-family: Title; 40 | src: url(assets/fonts/cocosharp/Coco-Sharp-Bold-trial.ttf); 41 | } 42 | 43 | @theme inline { 44 | --animate-aurora: aurora 60s linear infinite; 45 | 46 | @keyframes aurora { 47 | from { 48 | background-position: 49 | 50% 50%, 50 | 50% 50%; 51 | } 52 | 53 | to { 54 | background-position: 55 | 350% 50%, 56 | 350% 50%; 57 | } 58 | } 59 | } 60 | 61 | @layer base { 62 | :root { 63 | --background: 0 0% 100%; 64 | --foreground: 224 71.4% 4.1%; 65 | --card: 0 0% 100%; 66 | --card-foreground: 224 71.4% 4.1%; 67 | --popover: 0 0% 100%; 68 | --popover-foreground: 224 71.4% 4.1%; 69 | --primary: 220.9 39.3% 11%; 70 | --primary-foreground: 210 20% 98%; 71 | --secondary: 220 14.3% 95.9%; 72 | --secondary-foreground: 220.9 39.3% 11%; 73 | --muted: 220 14.3% 95.9%; 74 | --muted-foreground: 220 8.9% 46.1%; 75 | --accent: 220 14.3% 95.9%; 76 | --accent-foreground: 220.9 39.3% 11%; 77 | --destructive: 0 84.2% 60.2%; 78 | --destructive-foreground: 210 20% 98%; 79 | --border: 220 13% 91%; 80 | --input: 220 13% 91%; 81 | --ring: 224 71.4% 4.1%; 82 | --chart-1: 12 76% 61%; 83 | --chart-2: 173 58% 39%; 84 | --chart-3: 197 37% 24%; 85 | --chart-4: 43 74% 66%; 86 | --chart-5: 27 87% 67%; 87 | --radius: 0.5rem; 88 | } 89 | .dark { 90 | --background: 224 71.4% 4.1%; 91 | --foreground: 210 20% 98%; 92 | --card: 224 71.4% 4.1%; 93 | --card-foreground: 210 20% 98%; 94 | --popover: 224 71.4% 4.1%; 95 | --popover-foreground: 210 20% 98%; 96 | --primary: 210 20% 98%; 97 | --primary-foreground: 220.9 39.3% 11%; 98 | --secondary: 215 27.9% 16.9%; 99 | --secondary-foreground: 210 20% 98%; 100 | --muted: 215 27.9% 16.9%; 101 | --muted-foreground: 217.9 10.6% 64.9%; 102 | --accent: 215 27.9% 16.9%; 103 | --accent-foreground: 210 20% 98%; 104 | --destructive: 0 62.8% 30.6%; 105 | --destructive-foreground: 210 20% 98%; 106 | --border: 215 27.9% 16.9%; 107 | --input: 215 27.9% 16.9%; 108 | --ring: 216 12.2% 83.9%; 109 | --chart-1: 220 70% 50%; 110 | --chart-2: 160 60% 45%; 111 | --chart-3: 30 80% 55%; 112 | --chart-4: 280 65% 60%; 113 | --chart-5: 340 75% 55%; 114 | } 115 | } 116 | 117 | @layer base { 118 | * { 119 | @apply border-border; 120 | } 121 | body { 122 | @apply bg-background text-foreground; 123 | } 124 | } -------------------------------------------------------------------------------- /src/components/ui/following-pointer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import { motion, AnimatePresence, useMotionValue } from "motion/react"; 4 | import { cn } from "../../lib/utils"; 5 | 6 | export const FollowerPointerCard = ({ 7 | children, 8 | className, 9 | title, 10 | }: { 11 | children: React.ReactNode; 12 | className?: string; 13 | title?: string | React.ReactNode; 14 | }) => { 15 | const x = useMotionValue(0); 16 | const y = useMotionValue(0); 17 | const ref = React.useRef(null); 18 | const [rect, setRect] = useState(null); 19 | const [isInside, setIsInside] = useState(false); // Add this line 20 | 21 | useEffect(() => { 22 | if (ref.current) { 23 | setRect(ref.current.getBoundingClientRect()); 24 | } 25 | }, []); 26 | 27 | const handleMouseMove = (e: React.MouseEvent) => { 28 | if (rect) { 29 | const scrollX = window.scrollX; 30 | const scrollY = window.scrollY; 31 | x.set(e.clientX - rect.left + scrollX); 32 | y.set(e.clientY - rect.top + scrollY); 33 | } 34 | }; 35 | const handleMouseLeave = () => { 36 | setIsInside(false); 37 | }; 38 | 39 | const handleMouseEnter = () => { 40 | setIsInside(true); 41 | }; 42 | return ( 43 |
53 | 54 | {isInside && } 55 | 56 | {children} 57 |
58 | ); 59 | }; 60 | 61 | export const FollowPointer = ({ 62 | x, 63 | y, 64 | title, 65 | }: { 66 | x: any; 67 | y: any; 68 | title?: string | React.ReactNode; 69 | }) => { 70 | const colors = [ 71 | "#0ea5e9", 72 | "#737373", 73 | "#14b8a6", 74 | "#22c55e", 75 | "#3b82f6", 76 | "#ef4444", 77 | "#eab308", 78 | ]; 79 | return ( 80 | 100 | 110 | 111 | 112 | 132 | {title || `William Shakespeare`} 133 | 134 | 135 | ); 136 | }; 137 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Spotify Song Downloader - Free Spotify to MP3 (320kbps) 9 | 10 | 11 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 85 | 86 | 87 | 88 |

Spotify Song Downloader - Convert Spotify Songs, Playlists & Albums to MP3

89 |
90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/ui/lamp.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { motion } from "motion/react"; 4 | import { cn } from "../../lib/utils"; 5 | 6 | export default function LampDemo() { 7 | return ( 8 | 9 | 19 | Build lamps
the right way 20 |
21 |
22 | ); 23 | } 24 | 25 | export const LampContainer = ({ 26 | children, 27 | className, 28 | }: { 29 | children: React.ReactNode; 30 | className?: string; 31 | }) => { 32 | return ( 33 |
39 |
40 | 53 |
54 |
55 | 56 | 69 |
70 |
71 | 72 |
73 |
74 |
75 | 85 | {/* */} 95 | 96 |
97 |
98 | 99 |
100 | {children} 101 |
102 |
103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /src/components/ui/glowing-effect.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { memo, useCallback, useEffect, useRef } from "react"; 4 | import { cn } from "../../lib/utils"; 5 | import { animate } from "motion/react"; 6 | 7 | interface GlowingEffectProps { 8 | blur?: number; 9 | inactiveZone?: number; 10 | proximity?: number; 11 | spread?: number; 12 | variant?: "default" | "white"; 13 | glow?: boolean; 14 | className?: string; 15 | disabled?: boolean; 16 | movementDuration?: number; 17 | borderWidth?: number; 18 | } 19 | const GlowingEffect = memo( 20 | ({ 21 | blur = 0, 22 | inactiveZone = 0.7, 23 | proximity = 0, 24 | spread = 20, 25 | variant = "default", 26 | glow = false, 27 | className, 28 | movementDuration = 2, 29 | borderWidth = 1, 30 | disabled = true, 31 | }: GlowingEffectProps) => { 32 | const containerRef = useRef(null); 33 | const lastPosition = useRef({ x: 0, y: 0 }); 34 | const animationFrameRef = useRef(0); 35 | 36 | const handleMove = useCallback( 37 | (e?: MouseEvent | { x: number; y: number }) => { 38 | if (!containerRef.current) return; 39 | 40 | if (animationFrameRef.current) { 41 | cancelAnimationFrame(animationFrameRef.current); 42 | } 43 | 44 | animationFrameRef.current = requestAnimationFrame(() => { 45 | const element = containerRef.current; 46 | if (!element) return; 47 | 48 | const { left, top, width, height } = element.getBoundingClientRect(); 49 | const mouseX = e?.x ?? lastPosition.current.x; 50 | const mouseY = e?.y ?? lastPosition.current.y; 51 | 52 | if (e) { 53 | lastPosition.current = { x: mouseX, y: mouseY }; 54 | } 55 | 56 | const center = [left + width * 0.5, top + height * 0.5]; 57 | const distanceFromCenter = Math.hypot( 58 | mouseX - center[0], 59 | mouseY - center[1] 60 | ); 61 | const inactiveRadius = 0.5 * Math.min(width, height) * inactiveZone; 62 | 63 | if (distanceFromCenter < inactiveRadius) { 64 | element.style.setProperty("--active", "0"); 65 | return; 66 | } 67 | 68 | const isActive = 69 | mouseX > left - proximity && 70 | mouseX < left + width + proximity && 71 | mouseY > top - proximity && 72 | mouseY < top + height + proximity; 73 | 74 | element.style.setProperty("--active", isActive ? "1" : "0"); 75 | 76 | if (!isActive) return; 77 | 78 | const currentAngle = 79 | parseFloat(element.style.getPropertyValue("--start")) || 0; 80 | let targetAngle = 81 | (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / 82 | Math.PI + 83 | 90; 84 | 85 | const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180; 86 | const newAngle = currentAngle + angleDiff; 87 | 88 | animate(currentAngle, newAngle, { 89 | duration: movementDuration, 90 | ease: [0.16, 1, 0.3, 1], 91 | onUpdate: (value) => { 92 | element.style.setProperty("--start", String(value)); 93 | }, 94 | }); 95 | }); 96 | }, 97 | [inactiveZone, proximity, movementDuration] 98 | ); 99 | 100 | useEffect(() => { 101 | if (disabled) return; 102 | 103 | const handleScroll = () => handleMove(); 104 | const handlePointerMove = (e: PointerEvent) => handleMove(e); 105 | 106 | window.addEventListener("scroll", handleScroll, { passive: true }); 107 | document.body.addEventListener("pointermove", handlePointerMove, { 108 | passive: true, 109 | }); 110 | 111 | return () => { 112 | if (animationFrameRef.current) { 113 | cancelAnimationFrame(animationFrameRef.current); 114 | } 115 | window.removeEventListener("scroll", handleScroll); 116 | document.body.removeEventListener("pointermove", handlePointerMove); 117 | }; 118 | }, [handleMove, disabled]); 119 | 120 | return ( 121 | <> 122 |